一、ES6系列之let和const
在 ES6 以前,JS 只有 var 一种声明方式,但是在 ES6 之后,就多了 let 跟 const 这两种方式。用 var 定义的变量没有块级作用域的概念,而 let 跟 const 则会有,因为这三个关键字创建是不一样的。
变量提升
通过 var 声明的变量存在变量提升的特性,let 和 const 声明的变量不存在变量提升。
1 | if (false) { |
全局变量
当在全局作用域中使用 var 声明的时候,会创建一个新的全局变量作为全局对象的属性,而 let 和 const 不会。
1 | var carName = "BMW"; |
重复声明
var 可以重复声明,而 let 和 const 不能。
1 | var carName = "BMW"; |
重新赋值
var 和 let 可以重新赋值,const 不行
1 | var carName = "BMW"; |
暂时死区
暂时死区(Temporal Dead Zone),简写为 TDZ。
let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错:
1 | console.log(typeof value); // Uncaught ReferenceError: value is not defined |
这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
1 | var value = "global"; |
两个例子中,结果并不会打印 “global”,而是报错 Uncaught ReferenceError: value is not defined,就是因为 TDZ 的缘故。
只声明不初始化
在 ES6 中,const 定义的变量是必须要初始化赋值,而且以后不能变更, 是一个固定值。而像 var,let 是可以只声明,但是不进行初始化。
1 | var a; |
块级作用域
在 ES6 之前,是没有块级作用域的概念的,为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:
- 函数内部
- 块中(字符 { 和 } 之间的区域)
块级声明用于声明在指定块的作用域之外无法访问的变量,let 和 const 都是块级声明的一种。
1 | { |
循环中的块级作用域
下面看一个常见的面试题:
1 | var funcs = []; |
解决方案:
1 | var funcs = []; |
Es6 解决方案
1 | var funcs = []; |
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 | var funcs = []; |
相当于:
1 | // 伪代码 |
当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。
除了常规的 for 循环之外,还有 for-in 和 for-of 操作, 原理都是一样的,他们每一次的迭代都是重新声明一个全新的迭代对象,而不是给原来声明的迭代对象赋新值, 循环体内获取到的都是当前迭代对象的值。
1 | let funcs = []; |
现在看一下 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 | if (false) { |
如果还是直接编译成 var,打印的结果肯定是 undefined,然而 Babel 很聪明,它编译成了:
1 | if (false) { |
我们再写个直观的例子:
1 | let value = 1; |
1 | var value = 1; |
本质是一样的,就是改变量名,使内外层的变量名称不一样。
那像 const 的修改值时报错,以及重复声明报错怎么实现的呢?
其实就是在编译的时候直接给你报错……
那循环中的 let 声明呢?
1 | var funcs = []; |
Babel 巧妙的编译成了:
1 | var funcs = []; |
总结
最后用一个图表来展示他们之间的区别:
| var | let | const | |
|---|---|---|---|
| 变量提升 | √ | × | × |
| 全局变量 | √ | × | × |
| 重复声明 | √ | × | × |
| 重新赋值 | √ | √ | × |
| 暂时死区 | × | √ | √ |
| 只声明不初始化 | √ | √ | × |
| 块作用域 | × | √ | √ |