语言特性
作用域和作用域链
运行期上下文:当函数执行时,会创建一个称为执行上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行上下文被销毁。
执行上下文包含三个部分:
- 变量对象(VO,存储着变量和函数声明);
- 作用域链;
- this 指向
作用域:上下文中声明的变量和函数的作用范围。分为块级作用域和函数作用域。
每个 JavaScript 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 JavaScript 引擎存取,[[scope]] 就是其中之一。
[[scope]] 指的是我们所说的作用域,其中存储了执行期上下文的集合。
作用域链:[[scope]] 中存储的执行期上下文对象的集合,这个集合呈链式调用,我们把这种链式链接叫做作用域链。
JavaScript 中的作用域就是词法作用域。词法作 用域 是一套关于引擎如何寻找变量以及在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用
eval()和with)。词法作用域是由书写代码时函数声明的位置决定的。
函数作用域
JavaScript 具有基于函数的作用域,属于这个函数的全部变量都可以在整个函数的范围内使用。
动态作用域
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,它只关心 从何处调用。即:作用域链是基于调用栈的,而不是代码的作用域嵌套。
事实上 JavaScript 并不具有动态作用域,它只有词法作用域。但是 this 机制在某种程度上很像动态作用域。
块作用域
ES6 之前,可以使用 with 和 catch 或者立即执行函数(IIFE)的方式模拟块作用域,例如:
try{
throw 1;
}catch(e){
console.log(e); // 1
}
console.log(e); // 会报错
(function(){
// ...
})()
ES6 中引入了 let,可以创建完整的、不受约束的块作用域。let 关键字可以将变量绑定到所在的任意作用域中(通常是 {...} 内部)。即:let 为其声明的变量隐式地劫持了所在地块作用域。
代码执行过程
- 创建全局上下文;
- 全局执行上下文逐行自上而下执行,遇到函数时,函数执行上下文被 push 到执行栈顶层;
- 函数执行上下文被激活,开始执行函数中的代码,全局执行上下文被挂起;
- 函数执行完毕,函数执行上下文 pop 移除出执行栈,控制权交还给全局上下文,继续执行下面的代码。
闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
let const 与 var 的区别
let 与 const 的特性相似。两者与 var 的不同之处:
var声明的变量会进行变量提升,let和const不会变量提升,它们只在所在的代码块内有效(即{}内),提前获取let或者const声明的变量的值会报错,提前使用var声明的变量,值会是undefined;var可以重复声明变量,但let和const不能重复声明,这会报错。var声明之后再使用let或者const声明,或者用let和const声明之后再使用var重复声明也会报错;- 只要块级作用域内存在
let命令,它所声明的变量就“绑定”这个区域,不再受外部影响。在代码内,使用 let 声明变量之前,改变量是不可用的,在语法上,称为“暂时性死区”(temporal dead zone,TDZ)。 let实际上为 JavaScript 新增了块级作用域。
const 声明一个只读的常量,一旦声明,常量的值就不能改变。
块级作用域
ES6 之前,可以使用立即执行函数(IIFE)创建块级作用域。ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。
function fn(){
console.log('World!');
}
(function(){
if(false){ // 创建了块级作用域
function fn(){ // 重复生命一次 fn 函数
console.log('Hello!');
}
}
fn(); // 调用会报错!
// fn is not a function
})();
在符合 ES6 的浏览器中,上面都会报错,因为实际运行的是以下的代码:
function fn() {
console.log('World!');
}
(function () {
var fn = undefined;
if (false) { // 创建了块级作用域
function fn() { // 重复生命一次 fn 函数
console.log('Hello!');
}
}
fn(); // 调用会报错!
})();
原型链
对象之间通过原型关联到一起,就好比用一条锁链将一个个对象连接在一起,在与各个对象挂钩后,最终形成一条原型链。在读取对象的一个属性时,会先在对象中查询自有属性,如果不存在,那么会沿着原型链向 上搜索匹配的继承属性,直至找到或到达原型链顶端,才停止搜索。
this 指向
this 关注函数如何调用。this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时地各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(而不是声明位置)。
非严格模式下,this 默认绑定到 window 上,严格模式下 this 会绑定到 undefined。
箭头函数不能显示地绑定 this,即用 call、apply 和 bind 绑定 this 时,箭头函数会忽略第一个参数。
箭头函数并不关心 this 绑定。在箭头函数中使用 this 时,使用词法作用域中的 this。
undefined 和 null 的异同
相同部分:
- 不包含方法或属性;
- 都是假值;
- 都只有一个值;
- 都是“空缺”的意思;
不同之处:
- 含义不同,
undefined表示一个未定义的值,null表示一个空的对象; - 类型不同,typeof undefined 会得到
'undefined';而 typeof null 会得到object; - 数字转换不同,Number(undefined) 会得到
NaN;而 Number(null) 会得到0; - 在非严格模式下,undefined 能被当作变量来使用和赋值,而 null 不行。
空对象的强制类型转换
考虑下面代码,会打印出什么?
[] + {} = ?
{} + [] = ?
{} + 2 = ?
答案:
'[object Object]'
0
2
第一个比较好理解,[] 会转成字符串与 {} 相加,{} 也会转成字符串:
[] => "" // [] 转成空字符串
{} => '[object Object]'
第二个结果是数字 0。这是因为 {} 在表达式左侧时表示不执行任何操作的空代码块, + [] 相当于 +[],+A 运算会尝试将 A 转成数字。[] 转成数字是 0。
明白了第二个运算结果的由来,第三个语句也就明白了为什么是 2。
相关文档:
需要注意的是,如果把运算式用
console.log包裹, + [] 将变成'[object Object]',{}将不认为是空代码块,它会调用对象的toString方法,转成字符串。