本文知识点主要整理自《深入理解 ES6(Understanding ECMAScript 6)》中文版实体书的内容,部分地方会加上自己的理解,同时书中叙述比较模糊的部分参考了阮一峰老师的《ECMAScript 6 入门》与网络上其他大佬们的博客、问答,篇幅有限无法一一列出,在此表示感谢。
var 声明及变量提升机制
变量提升(Hoisting)
在函数作用域或者全局作用域中使用 var
声明的变量,无论是在哪里进行声明,都会被当成在当前作用域的顶部进行的变量声明,这就是变量提升(Hoisting)。
如:
在函数 temp
中声明局部变量,与在函数 temp
后声明全局变量
function temp (condition) {
if (condition) {
var a = 1; // 函数的局部变量
} else {
console.log(a); // undefined
}
}
var b = 2; // 全部变量
复制代码
当预编译的时候,实际上是将上面的代码转化为:
var b; // 全局变量 b 被提升到全局作用于顶部进行了声明
function temp (conditionP {
var a; // 局部变量 a 被提升到函数作用于顶部进行了声明
if (condition) {
a = 1;
} else {
console.log(a); // undefined
}
}
b = 2;
复制代码
同时由于变量提升的原因,即便在 temp
中 condition
为 false
,在其 else
分支内仍然可以访问到 a
。
块级声明
由于变量提升的存在,对于接触 JavaScript 的人来说难免会不太习惯,甚至会导致一些 bug。因此 ES6 加入了块级作用域来对变量的声明周期进行控制。
ES6 的块级声明作用域指定的区块之间,主要有:
- 函数内部
- 块中(字符 { 与 } 之间)
let 声明
let
的用法与 var
相同,使用 let
进行声明就能将变量的作用域限制在区块中,不会进行变量提升。
如果上文的代码中,a
使用 let
进行声明的话,那么在 if
语句中 condition
为 false
时,else
分支将无法访问到 a
的值。
if (condition) {
let a = 1;
} else {
console.log(a); // Uncaught ReferenceError: a is not defined
}
复制代码
禁止重复声明
在 同一个作用域内,不能使用 let
再次声明一个已经存在的变量,不论该变量原先是用什么关键字声明的。
var a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared
复制代码
同样,如果是先使用 let
声明,再使用其他关键字声明,同样会报错。
如果都是使用
var
关键字则不会报错
如果是在当前作用域下内嵌另一个作用域,则可以在内嵌作用域中声明与父级作用域同名的变量,不过在内嵌作用域中的变量在内嵌作用域内会覆盖掉父级作用域的变量值。
const 声明
由 const
声明的是常量,常量一旦被赋值之后便不可改变。因此,每一个常量在被声明的同时 必须进行初始化。
const
与 let
类似,都不会产生变量提升,并且声明的变量都只作用于域块级作用域。同时,如果采用 const
声明已被声明的变量,同样会报错。
与 let
不同的是,不论严格模式或者非严格模式下,const
声明过的变量都不能对其 值 进行改变。
使用 const 声明对象
与其他的语言中的常量很不同的一点,const
声明的常量虽然不能够改变值,但当使用 const
声明对象时,可以改变对象的属性值。
const person = {
name: 'Jack',
};
person.name = 'Harry'; // 不会报错
person = { name: 'Peter' }; // 报错
复制代码
在 JavaScript 中,对象是引用数据类型,即上面代码中 person
存储的其实是对象的引用(引用可以理解为内存地址)。而对 person.name
的值的改变是改变 person
引用的对象,对 person
这个 引用 本身并未作出修改,因此使用 const
声明的 person
并未报错。
而当使用 { name: 'Peter' }
对 person
进行修改时,实际上是将 person
的引用 更改 到新对象的内存地址,person
的值被改变了,因此报错。
临时死区
当使用 let
或者 const
声明参数时,在预编译阶段,JavaScript 引擎会将这些参数绑定到其对应的作用域内,并且在执行到声明语句之前如果对这些变量进行操作 均会报错,即便是 typeof
。
我们将变量在声明之前所处的封闭区域成为 临时死区 或 暂时性死区 (Temporal Dead Zone,简称 TDZ)。
JavaScript 引擎在预编译时,会将变量提升到作用域顶部(var
),或将变量放入 TDZ(let
或 const
)。访问 TDZ 中的变量会报错。当执行变量声明语句后,变量才会从 TDZ 中移除。
注意,TDZ 是绑定作用域的。如果在 TDZ 外访问变量则不会报错。如:
console.log(typeof param); // 输出 'undefined',此处的 param 为全局变量,预编译的时候会有变量提升,因此是 undefined
// JavaScript 引擎在预编译到 if 区块时,会创建一个对应的 TDZ 并且把 param 加入其中
if (condition) {
param = 9; // 报错,访问了 if 区块内的 TDZ 内的变量 param
let param = 1; // 此时 param 从 TDZ 中移出,可以访问
}
复制代码
循环中的块作用域绑定
在 ES5 中,在循环内部声明的变量,在循环外部仍旧可以访问。
for (var i = 0; i < 10; i += 1) {
console.log(i);
}
console.log(i); // 10;
复制代码
这也是由于变量提升, i
的声明在预编译时被提到全局作用域顶部,因此在循环结束后外部仍旧可以访问 i
。
如果采用 let
声明的话,那么 i
在循环结束后就会被销毁,外部无法进行访问。
for (let i = 0; i < 10; i += 1) {
console.log(i);
}
console.log(i); // 报错,无法访问 i
复制代码
循环中的函数
var funcs = [];
for (var i = 0; i < 10; i += 1) {
funcs.push(function () {
console.log(i);
});
}
funcs.forEach(function (func) {
func();
});
复制代码
以上代码执行后会输出 10 个 10。这是由于变量提升的原因,i
提升到作用域的顶部,并且在循环外也能够访问,在 for
循环执行完毕后,i
的值为 10,因此在 forEach
遍历数组内的函数并且执行时,均输出 10。
在 ES5 中问了解决该问题,常用的方式是使用 立即调用函数表达式(IIFE)。具体写法如下:
var funcs = [];
for (var i = 0; i < 10; i += 1) {
funcs.push((function (value) {
return function () {
console.log(value);
}
} (i)));
}
funcs.forEach(function (func) {
func(); // 0, 1, 2, ..., 10
});
复制代码
在 IIFE 中,将 i
进行值传递(函数的传参是值传递),创建副本并且存储为变量 value
,因此才能够实现正确输出。
循环中的 let 声明
在 ES6 中,可以在 for
循环中直接使用 let
关键字声明,来达到和 IIFE 一样的效果。
for (let i = 0; i < 10; i+= 1) {
funcs.push(function () {
console.log(i);
})
}
复制代码
在 for
循环中,声明赋值语句 let i = 0
仅在循环之前执行一次(并且这一句执行的时候是在 父级 作用域,与循环内部的作用域是分开的),在执行循环时, JavaScript 内部会记录当前循环的值,在进入下一轮时,会 创建一个新的值,并且用记住的值进行计算并且进入下一轮。因此使用 let
关键词声明的循环在每一次循环时都会得到一个属于该次循环的 副本。
for-in
语句使用 let
关键字是也是同样的效果。
let
声明在循环内部的表现是专门定义的,不一定与不产生变量提升的特性相关。
循环中的 const 声明
// 下面会报错
for (const i = 0; i < 10; i += 1) {
...
}
// 下面则不会报错
for (const i in obj) {
...
}
复制代码
对于 for
循环来说,每次循环执行完毕,都会去修改 i
的值,然后再创建循环体内块级作用域的副本,因此在 for
循环用 const
声明时会报错。
而 for-in
与 for-of
在每次迭代时,是每次都会在 新的作用域内 执行 const
声明,不会修改值,因此不会报错。
全局块作用域绑定
使用 var
进行全局变量声明时,会为全局对象创建一个新的属性。以 Web 为例:
var apple = 1;
console.log(window.apple); // 1
复制代码
如果全局对象已经存在属性,则会进行 覆盖(这是一个隐患)。
使用 let
与 const
在全局作用域声明时,会创建一个新的绑定,并且不会覆盖全局对象已有的属性。
let RepExp = 'Hello';
console.log(RepExp); // Hello
console.log(window.RepExp === RepExp); // false
复制代码
最佳实践
在 ESLint 等代码规范工具中,推荐的写法是 默认使用 const
,在确实需要改变值或者引用的时候才使用 let
。虽然比较繁琐,但是能够有效降低一些隐性 bug 出现的概率。
详见 ESLint 的 no-var 与 prefer-const 规则
参考资料
- Understand EcmaScript 6
- ECMAScript 6 入门 - let 和 const 命令