块级声明
块指由花括号封闭的代码块。
块级声明包括 let
和 const
两种声明方式,首先介绍它们之间的共性特点:
- 块级作用域:块级声明的变量,无法在指定的块作用域外访问;
- 禁止重复声明:如果一个标识符已经在代码块内部被定义,那么禁止在此代码块内部对同一个标识符进行块级声明,否则会抛出错误;
- 暂时性死区:块级声明的变量不仅不会进行变量提升,在代码运行到声明处之前还会被存放在暂时性死区(TDZ)的区域内。如果在声明处之前试图访问变量会导致一个引用错误,即使是使用
typeof
运算符也是如此!
let
和 const
之间的差异在于,用 const
声明的变量会被认为是常量,意味着它们的值在被设置完成后就不能再被改变。如果使用 const
声明了一个对象,变量中保存的是对象的引用,所以只要不重写对象,其他的也都随意更改。
循环中的块级绑定
开发者最需要使用变量的块级作用域的场景,或许就是在 for
循环内,也就是想让一次性的循环计数器只能在循环内部使用。例如,以下代码在 JavaScript 中并不罕见:
1 | for (var i = 0; i < 10; i++) { |
在其他默认使用块级作用域的语言中,这个例子能够按照预期工作,也就是只有 for
才能访问变量 i
。然后在JavaScript中,循环结束后 i
仍然可被访问,因为 var
声明导致了变量提升。若像如下代码那样换为使用 let
,则会看到逾期行为:
1 | for (let i = 0; i < 10; i++) { |
循环内的函数
长期以来,var
的特点使得循环变量在循环作用域外仍然可被访问,于是在循环内部创建函数就变得很有问题。考虑如下代码:
1 | var funcs = [] |
为了修正上述问题,开发者在循环内使用立即调用函数表达式(IIFE),以便在每次迭代中强制创建变量的一个新副本,示例如下:
1 | var funcs = [] |
虽然解决了问题,但是这种写法相对繁琐。幸运的是,使用 let
与 const
的块级绑定可以在 ES6 中为你简化这个循环。
循环内的 let 声明
let
声明通过有效模仿上例中的 IIFE 的作用而简化了循环。在每次迭代中,都会创建一个新的同名变量并对其进行初始化。这意味着你可以完全省略 IIFE 从而获得预期的结果,就像这样:
1 | var funcs = [] |
与使用 var
声明以及 IIFE 相比,这里代码能达到相同效果,但无疑更加简洁。在循环中 let
声明每次都创建了一个新的 i
变量,因此在循环内部创建的函数获得了各自的 i
副本,而每个 i
副本的值都在每次循环迭代声明变量的时候被确定了。这种方式在 for-in
和 for-of
循环中同样适用,如下所示:
1 | var funcs = [] |
本例中的 for-in
循环体现出了与 for
循环相同的行为。每次循环,一个新的 key
变量绑定就被创建,因此每个函数都能够拥有它自身的 key
变量副本,结果每个函数都输出了一个不同的值。
需要重点了解的是:let
声明在循环内部的行为是在规范中特别定义的,而与不提升变量声明的特征没有必然联系。事实上,在早期 let
的实现中并没有这种行为,它是后来才添加的。
循环内的 const 声明
ES6规范没有明确禁止在循环中使用 const
声明,然而它会根据循环方式的不同而有不同行为。在常规的 for
循环中,你可以在初始化时使用 const
,但循环会在你试图改变该变量的值时抛出错误。例如:
1 | var funcs = [] |
在此代码中,i
被声明为一个常量。循环的第一次迭代成功执行,此时 i
的值为0。在 i++
执行时,一个错误会被抛出,因为该语句试图更改常量的值。因此,在循环中你只能使用 const
来声明一个不会被更改的变量。
而另一方面,const
变量在 for-in
或 for-of
循环中使用时,与 let
变量效果相同。因此下面代码不会导致出错:
1 | var funcs = [] |
这段代码与“循环内的 let 声明”小节的第二个例子几乎完全一样,唯一的区别是 key
的值在循环内不能被更改。const
能够在 for-in
与 for-of
循环内工作,是因为循环为每次迭代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。
全局块级绑定
let
与 const
不同于 var
的另一个方面是在全局作用域上的表现。当在全局作用域上使用 var
时,它会创建一个新的全局变量,并成为全局对象(在浏览器中是 window
)的一个属性。这意味着使用 var
可能会无意覆盖一个已有的全局属性,就像这样:
1 | // 在浏览器中 |
尽管全局的 RegExp
是定义在 window
上的,它仍然不能防止被 var
重写。这个例子声明了一个新的全局变量 RegExp
而覆盖了原有对象。类似地,ncz
定义为全局变量后就立即成为了 window
的一个属性。
然而若你在全局作用域上使用 let
或 const
,虽然在全局作用域上会创建新的绑定,但不会有任何属性被添加到全局对象上。这也就意味着你不能使用 let
或 const
来覆盖一个全局变量,你只能将其屏蔽。这里有个范例:
1 | // 在浏览器中 |
此代码的 let
声明创建了 RegExp
的一个绑定,并屏蔽了全局的 RegExp
。这表示 window.RegExp
与 RegExp
是不同的,因此全局作用域没有被污染。同样,const
声明创建了 ncz
的一个绑定,但并未在全局对象上创建属性。当你不想在全局对象上创建属性时,这种特性会让 let
与 const
在全局作用域中更安全。
块级绑定新的最佳实践
在默认情况下使用 const
,并且只在知道变量值需要被更改的情况下才使用 let
。