# ES6 系列之 let 和 const
# 块级作用域的出现
声明变量的时候,一定要使用var命令,不然会声明一个全局变量。
function f1() {
n = 999;
}
f1();
console.log(n); // 999
2
3
4
5
为什么需要块级作用域? ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefine
2
3
4
5
6
7
8
9
10
通过 var 声明的变量存在变量提升的特性:
var condition = false;
if (condition) {
var value = 1;
}
console.log(value); // undefined
2
3
4
5
初学者可能会觉得只有 condition 为 true 的时候,才会创建 value,如果 condition 为 false,结果应该是报错,然而因为变量提升的原因,代码相当于:
var condition = false;
var value;
if (condition) {
value = 1;
}
console.log(value); // undefined
2
3
4
5
6
即便循环已经结束了,我们依然可以访问 i 的值。
为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:
函数内部 块中(字符 { 和 } 之间的区域)
# let 和 const
- 块级声明用于声明在指定块的作用域之外无法访问的变量。
- let 和 const 都是块级声明的一种。
我们来回顾下 let 和 const 的特点:
# 1. 不会被提升
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
2
3
4
# 2. 重复声明报错
var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
2
# 3. 不绑定全局作用域
当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性。
var value = 1;
console.log(window.value); // 1
2
然而 let 和 const 不会:
let value = 1;
console.log(window.value); // undefined
2
再来说下 let 和 const 的区别:
const 用于声明常量,其值一旦被设定不能再被修改,否则会报错。
值得一提的是:const 声明不允许修改绑定,但允许修改值。这意味着当用 const 声明对象时:
于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const data = {
value: 1
}
// 没有问题
data.value = 2;
data.num = 3;
// 报错
data = {}; // Uncaught TypeError: Assignment to constant variable.
2
3
4
5
6
7
8
9
10
# 临时死区
临时死区(Temporal Dead Zone),简写为 TDZ。
- 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
- 总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”。
let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:
var value = "global";
// 例子1
(function() {
console.log(value); // ReferenceError: value is not defined
let value = 'local';
}());
// 例子2
{
console.log(value); // ReferenceError: value is not defined
const value = 'local';
};
2
3
4
5
6
7
8
9
10
11
12
13
两个例子中,结果并不会打印 "global",而是报错 Uncaught ReferenceError: value is not defined,就是因为 TDZ 的缘故。
# 循环中的块级作用域
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function() {
console.log(i);
};
}
funcs[0](); // 3
2
3
4
5
6
7
一个老生常谈的面试题,解决方案如下:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(i) {
return function() {
console.log(i);
}
}(i))
}
funcs[0](); // 0
2
3
4
5
6
7
8
9
ES6 的 let 为这个问题提供了新的解决方法:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log(i);
};
}
funcs[0](); // 0
2
3
4
5
6
7
问题在于,上面讲了 let 不提升,不能重复声明,不能绑定全局作用域等等特性,可是为什么在这里就能正确打印出 i 值呢?
如果是不重复声明,在循环第二次的时候,又用 let 声明了 i,应该报错呀,就算因为某种原因,重复声明不报错,一遍一遍迭代,i 的值最终还是应该是 3 呀,还有人说 for 循环的 设置循环变量的那部分是一个单独的作用域,就比如:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
2
3
4
5
6
7
这个例子是对的,如果我们把 let 改成 var 呢?
for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc
2
3
4
5
为什么结果就不一样了呢,如果有单独的作用域,结果应该是相同的呀……
如果要追究这个问题,就要抛弃掉之前所讲的这些特性!这是因为 let 声明在循环内部的行为是标准中专门定义的,不一定就与 let 的不提升特性有关,其实,在早期的 let 实现中就不包含这一行为。
# 我们会发现,在 for 循环中使用 let 和 var,底层会使用不同的处理方式。
那么当使用 let 的时候底层到底是怎么做的呢?
简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域,这就可以解释为什么:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
2
3
4
5
6
7
然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。这样对于下面这样一段代码
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log(i);
};
}
funcs[0](); // 0
2
3
4
5
6
7
就相当于:
// 伪代码
(let i = 0) {
funcs[0] = function() {
console.log(i)
};
}
(let i = 1) {
funcs[1] = function() {
console.log(i)
};
}
(let i = 2) {
funcs[2] = function() {
console.log(i)
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。
# 循环中的 let 和 const
不过到这里还没有结束,如果我们把 let 改成 const 呢?
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs[i] = function() {
console.log(i);
};
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.
2
3
4
5
6
7
结果会是报错,因为虽然我们每次都创建了一个新的变量,然而我们却在迭代中尝试修改 const 的值,所以最终会报错。
说完了普通的 for 循环,我们还有 for in 循环呢~
那下面的结果是什么呢?
var funcs = [],
object = {
a: 1,
b: 1,
c: 1
};
for (var key in object) {
funcs.push(function() {
console.log(key)
});
};
funcs[0]() // c
2
3
4
5
6
7
8
9
10
11
12
结果是 'c';
那如果把 var 改成 let 或者 const 呢?
使用 let,结果自然会是 'a',const 呢? 报错还是 'a'?
结果是正确打印 'a',这是因为在 for in 循环中,每次迭代不会修改已有的绑定,而是会创建一个新的绑定。
# 最佳实践
TIP
在我们开发的时候,可能认为应该默认使用 let 而不是 var ,这种情况下,对于需要写保护的变量要使用 const。然而另一种做法日益普及:默认使用 const,只有当确实需要改变变量的值的时候才使用 let。这是因为大部分的变量的值在初始化后不应再改变,而预料之外的变量之的改变是很多 bug 的源头。
# ES6 系列目录地址:
- ES6 系列为了加深自己对ES6知识的理解,梳理知识结构。系列目录地址: