循环中的块级绑定
开发者最需要使用变量的块级作用域的场景,或许就是在 for 循环内,也就是想让一次性的循环计数器仅能在循环内部使用。例如,以下代码在 JS 中并不罕见:
for (var i = 0; i < 10; i++) {
process(items[i]);
}
// i 在此处仍然可被访问
console.log(i); // 10
在其他默认使用块级作用域的语言中,这个例子能够照预期工作,也就是只有 for 才能访问变量 i。然而在 JS 中,循环结束后 i 仍然可被访问,因为 var 声明导致了变量提升。若像如下代码那样换为使用 let,则会看到预期行为:
for (let i = 0; i < 10; i++) {
process(items[i]);
}
// i 在此处不可访问, 抛出错误
console.log(i);
本例中的变量 i 仅在 for 循环内部可用,一旦循环结束,该变量在任意位置都不可访问。
循环内的函数
长期以来,var 的特点使得循环变量在循环作用域之外仍然可被访问,于是在循环内创建函数就变得很有问题。考虑如下代码:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function() { console.log(i); });
}
funcs.forEach(function(func) {
func(); // 输出数值 "10" 十次
});
你原本可能预期这段代码会输出 0 到 9 的数值,但它却在同一行将数值 10 输出了十次。这是因为变量 i 在循环的每次迭代中都被共享了,意味着循环内创建的那些函数都拥有对于同一变量的引用。在循环结束后,变量 i 的值会是 10,因此当 console.log(i) 被调用时,每次都打印出 10。
为了修正这个问题,开发者在循环内使用立即调用函数表达式(IIFEs),以便在每次迭代中强制创建变量的一个新副本,示例如下:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value);
}
}(i)));
}
funcs.forEach(function(func) {
func(); // 从 0 到 9 依次输出
});
这种写法在循环内使用了 IIFE。变量 i 被传递给 IIFE,从而创建了 value 变量作为自身副本并将值存储于其中。value 变量的值被迭代中的函数所使用,因此在循环从 0 到 9 的过程中调用每个函数都返回了预期的值。幸运的是,使用 let 与 const 的块级绑定可以在 ES6 中为你简化这个循环。
循环内的let声明
let 声明通过有效模仿上例中 IIFE 的作用而简化了循环。在每次迭代中,都会创建一个新的同名变量并对其进行初始化。这意味着你可以完全省略 IIFE 而获得预期的结果,就像这样:
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 从 0 到 9 依次输出
})
与使用 var 声明以及 IIFE 相比,这里代码能达到相同效果,但无疑更加简洁。在循环中 let 声明每次都创建了一个新的 i 变量,因此在循环内部创建的函数获得了各自的 i 副本,而每个 i 副本的值都在每次循环迭代声明变量的时候被确定了。这种方式在 for-in 和 for-of 循环中同样适用,如下所示:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
本例中的 for-in 循环体现出了与 for 循环相同的行为。每次循环,一个新的 key 变量绑定就被创建,因此每个函数都能够拥有它自身的 key 变量副本,结果每个函数都输出了一个不同的值。而如果使用 var 来声明 key,则所有函数都只会输出 "c"。
let 声明在循环内部的行为是在规范中特别定义的,而与不提升变量声明的特征没有必然联系。事实上,在早期 let 的实现中并没有这种行为,它是后来才添加的。 |
循环内的常量声明
ES6 规范没有明确禁止在循环中使用 const 声明,然而它会根据循环方式的不同而有不同行为。在常规的 for 循环中,你可以在初始化时使用 const,但循环会在你试图改变该变量的值时抛出错误。例如:
var funcs = [];
// 在一次迭代后抛出错误
for (const i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
在此代码中,i 被声明为一个常量。循环的第一次迭代成功执行,此时 i 的值为 0。在 i++ 执行时,一个错误会被抛出,因为该语句试图更改常量的值。因此,在循环中你只能使用 const 来声明一个不会被更改的变量。
而另一方面,const 变量在 for-in
或 for-of
循环中使用时,与 let 变量效果相同。因此下面代码不会导致出错:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
// 不会导致错误
for (const key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
这段代码与 “循环内的 let 声明” 小节的第二个例子几乎完全一样,唯一的区别是 key 的值在循环内不能被更改。const 能够在 for-in
与 for-of
循环内工作,是因为循环为每次迭代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值(就像使用了 for 而不是 for-in 的上个例子那样)。