一、ES6系列之let和const

在 ES6 以前,JS 只有 var 一种声明方式,但是在 ES6 之后,就多了 let 跟 const 这两种方式。用 var 定义的变量没有块级作用域的概念,而 let 跟 const 则会有,因为这三个关键字创建是不一样的。

变量提升

通过 var 声明的变量存在变量提升的特性,let 和 const 声明的变量不存在变量提升。

1
2
3
4
5
6
7
8
9
10
11
12
if (false) {
var carName = "BMW";
}
console.log(carName); // undefined
if (false) {
let personName = "Tom";
}
console.log(personName); // Uncaught ReferenceError: personName is not defined
if (false) {
const dogName = "Angle";
}
console.log(dogName); // Uncaught ReferenceError: dogName is not defined

全局变量

当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性,而 let 和 const 不会。

1
2
3
4
5
6
7
var carName = "BMW";
console.log(window.carName); // BMW

let personName = "Tom";
console.log(window.personName); // undefined
const dogName = "Angle";
console.log(window.dogName); // undefined

重复声明

var 可以重复声明,而 let 和 const 不能。

1
2
3
4
var carName = "BMW";
var carName = "BMW2";
let carName = "BMW3"; //Uncaught SyntaxError: Identifier 'carName' has already been declared
const carName = "BMW4"; //Uncaught SyntaxError: Identifier 'carName' has already been declared

重新赋值

var 和 let 可以重新赋值,const 不行

1
2
3
4
5
6
var carName = "BMW";
carName = "BB";
let personName = "Tom";
personName = "Jay";
const dogName = "Angle";
dogName = "Come"; // Uncaught TypeError: Assignment to constant variable.

暂时死区

暂时死区(Temporal Dead Zone),简写为 TDZ。
let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:

1
2
console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;

这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var value = "global";

// 例子1
(function () {
console.log(value);

let value = "local";
})();

// 例子2
{
console.log(value);

const value = "local";
}

两个例子中,结果并不会打印 “global”,而是报错 Uncaught ReferenceError: value is not defined,就是因为 TDZ 的缘故。

只声明不初始化

在 ES6 中,const 定义的变量是必须要初始化赋值,而且以后不能变更, 是一个固定值。而像 var,let 是可以只声明,但是不进行初始化。

1
2
3
4
5
6
var a;
let b;
console.log(a); // => undefined
console.log(b); // => undefined

const c; // Uncaught SyntaxError: Missing initializer in const declaration

块级作用域

在 ES6 之前,是没有块级作用域的概念的,为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:

  • 函数内部
  • 块中(字符 { 和 } 之间的区域)

块级声明用于声明在指定块的作用域之外无法访问的变量,let 和 const 都是块级声明的一种。

1
2
3
4
5
6
7
8
9
{
var x = 2;
}
console.log(x); // 2

{
let y = 2;
}
console.log(y); // Uncaught ReferenceError: y is not defined

循环中的块级作用域

下面看一个常见的面试题:

1
2
3
4
5
6
7
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
console.log(funcs[0]()); // 3

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var funcs = [];
for (var i = 0; i < 3; i++) {
// funcs[i] = (function (i) {
// return function () {
// console.log(i);
// };
// })(i);
(function (i) {
funcs[i] = function () {
console.log(i);
};
})();
}
console.log(funcs[0]()); // 0

Es6 解决方案

1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
console.log(funcs[0]()); // 0

let 声明的变量只在 for 的循环体中有效,循环结束后 变量就消失了, 同时 const 也可以在 for 循环中声明变量,但是不能用于 常规的 for 循环中。所谓的常规 for 循环就是 for(let i =0; i < 3; i++) 的格式,否则会报错。

在使用 for 循环的时候,每一次的迭代都会重新声明一个变量。像 for(let i = 0; i < 3; i++); 这样使用时,i 变量声明了 3 次,只不过每一次迭代给 i 赋值不一样而已,并且变量只在循环体中使用。 我们可以这样理解: 第一次迭代的使用,声明了一个变量 i, 赋值为 0, 0 < 3, 然后执行循环体,执行完之后 i++ 变成了 1. 这一次迭代就结束了,这个 i 的使命就完成了。然后进行第二次迭代,这时重新声明一个变量 i, 不过这次给他赋值为 1,1 < 3 继续执行循环体,然后加 1. 这次迭代又结束了,这个 i 也完成了使命,消失了。第三次迭代进行同样的操作,声明一个全新的变量 i,执行循环体之类的,直达整个循环结束。

所以对于 for 循环来说了,每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。,这样对于这一段代码:

1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0

相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 伪代码
(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)
};
};

当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。

除了常规的 for 循环之外,还有 for-in 和 for-of 操作, 原理都是一样的,他们每一次的迭代都是重新声明一个全新的迭代对象,而不是给原来声明的迭代对象赋新值, 循环体内获取到的都是当前迭代对象的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let funcs = [];
let arr = [1, 2, 3];
// for-in 循环, 数组是不建议使用for-in ,这里只是简单的演示
for (let key in arr) {
funcs.push(function () {
console.log(key);
});
}
funcs.forEach(function (func) {
func(); // 输出0, 1, 2
});

// 使用for-of
funcs = [];
for (let key of arr) {
funcs.push(function () {
console.log(key);
});
}

funcs.forEach(function (func) {
func(); // 输出1, 2,3
});

现在看一下 const, const 也可以使用在 for 循环中。最简单的就是把上面的三个 for 循环中的 let 都转换为 const. for (const i = 0; i < 3; i++) {}; for (const key of arr) {} , for (const key in arr) {} . 这时你会发现第一种常规 for 循环报错了。看一下第一次迭代就知道了。声明了一个 变量 i, 赋值为 0。 但这里使用 const, 也就意味着 i 在声明之后,就不能再改变了。好了,0 < 3, 执行循环体,然后 加 1,报错了,i 不能变化了。一次迭代都没有走完,就报错了,说明,在使用常规 for 循环时, const 不能用来声明变量。

再来看一下,for-of, for-in, 没有问题,因为每一次的迭代都会声明一个全新的 key, 所有的赋值都是给一个新的变量赋值,而没有改变原来的值。那使用 let 和 const 有什么区别吗? 当然有了,还是在于 const 声明的变量不能重新赋值了,所以如果 for-in 或 for- of 中使用 const 声明了变量( 如 key), 循环体中,就不能给 key 赋新值了,如果使用 let ,那就无所谓了,想干什么就干什么。只不过 for-in 或 for-of 中,我们很少改变 key 值,所以他们在实际使用时就没有什么区别了。

Babel

在 Babel 中是如何编译 let 和 const 的呢?我们来看看编译后的代码:

1
let value = 1;

编译为:

1
var value = 1;

我们可以看到 Babel 直接将 let 编译成了 var,如果是这样的话,那么我们来写个例子:

1
2
3
4
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined

如果还是直接编译成 var,打印的结果肯定是 undefined,然而 Babel 很聪明,它编译成了:

1
2
3
4
if (false) {
var _value = 1;
}
console.log(value);

我们再写个直观的例子:

1
2
3
4
5
let value = 1;
{
let value = 2;
}
value = 3;
1
2
3
4
5
var value = 1;
{
var _value = 2;
}
value = 3;

本质是一样的,就是改变量名,使内外层的变量名称不一样。

那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?

其实就是在编译的时候直接给你报错……

那循环中的 let 声明呢?

1
2
3
4
5
6
7
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0

Babel 巧妙的编译成了:

1
2
3
4
5
6
7
8
9
10
11
12
var funcs = [];

var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};

for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0

总结

最后用一个图表来展示他们之间的区别:

var let const
变量提升 × ×
全局变量 × ×
重复声明 × ×
重新赋值 ×
暂时死区 ×
只声明不初始化 ×
块作用域 ×

参考文献