背景 - 一个现象
for (var i=0; i<10; i++){
process(items[i]);
}
// i在此处仍然可被访问
console.log(i); // 10
上面现象:JS中,变量 i 泄露为全局变量,循环结束后 i 仍可被访问,因为 var 声明导致了变量提升。
我们最需要使用变量的块级作用域的场景,或许就是在for循环内 -- 想让一次性的循环计数器仅能在循环内部使用。
换为使用 let ,则会看到预期行为:
for (let i=0; i<10; i++) {
process(items[i]);
}
// i 在此处不可访问,抛出错误
console.log(i); // Uncaught ReferenceError: process is not defined
本例中的变量 i 仅在 for 循环内部可用,一旦循环结束,该变量在任意位置都不可访问。
一、循环内的函数问题
- ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5 的规定都是非法的。
但,浏览器没有遵守这个规定,为兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
- ES6 引入了块级作用域,明确允许在块级作用域中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。(后续有补充)
- 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 。
- 为修正 3. 中这个问题,我们想到了在循环内使用立即调用函数表达式(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 变量作为i的 副本并将值存储于其中。
value 变量的值被迭代中的函数所使用,因此在循环从 0 到 9 的过 程中调用每个函数都返回了预期/期望的值。
- 在 ES6 中,使用 let 与 const 的块级绑定可以简化这个循环。
二、循环内的 let 声明
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 = [],
obj = {
a: true,
b: true,
c: true
};
for (let key in obj) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
for-in 循环体现出了与 for 循环相同的行为。
每次循环,一个新的 key 变量绑 定就被创建,因此每个函数都能够拥有它自身的 key 变量副本,结果每个函数都输出了一个 不同的值。
而如果使用 var 来声明 key ,则所有函数都只会输出 "c" 。
需要重点了解的是: let 声明在循环内部的行为是在规范中特别定义的,而与不提升变 量声明的特征没有必然联系。事实上,在早期 let 的实现中并没有这种行为,它是后来 才添加的。
三、循环内的const声明
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 = [],
obj = {
a: true,
b: true,
c: true
};
// 不会导致错误
for (const key in obj) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
这段代码与“循环内的 let 声明”中几乎完全一样,唯一的区别是 key 的值在 循环内不能被更改。
const 能够在 for-in 与 for-of 循环内工作,是因为循环为每次迭 代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。
PS:let 、const 与 var 在全局作用域上的表现区别。
当在全局作用域上使 用 var 时,var会创建一个新的全局变量,并成为全局对象(在浏览器中是 window )的一 个属性。这意味着使用 var 可能会无意覆盖一个已有的全局属性,
// 在浏览器中
var RegExp = "World!";
console.log(window.RegExp ); //"World!"
var hisName= "ZhangSan!";
console.log(window.hisName); // "ZhangSan!"
虽然全局的 RegExp 是定义在 window 上的,但它仍不能防止被 var 重写。上例声明 了一个新的全局变量 RegExp 而覆盖了window上原有的RegExp 对象。
类似的, hisName定义为全局变量后就立即 成为了 window 的一个属性。这就是 JS 通常的工作方式。
区别:在全局作用域上使用 let 或 const声明变量 ,虽在全局作用域上会创建新的绑定,但不 会有任何属性被添加到全局对象上。这也就意味着你不能使用 let 或 const 来覆盖一个全 局变量,你只能将其屏蔽。这里有个范例:
// 在浏览器中
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false
const herName= "Lisi!";
console.log(herName); // "Lisi!"
console.log("herName" in window); // false
let 声明创建了 RegExp 的一个绑定,并屏蔽了全局的 RegExp 。这表示 window.RegExp 与 RegExp 是不同的,因此全局作用域没有被污染。
同样, const 声明创建 了 herName的一个绑定,但并未在全局对象上创建属性。当不想在全局对象上创建属性时,这 种特性会让 let 与 const 在全局作用域中更安全。
若想让代码能从全局对象中被访问,你仍然需要使用 var 。在浏览器中跨越帧或窗口去 访问代码时,这种做法非常普遍。
总结:
let 与 const 的很多情况下都相似于 var ,但在循环中和全局作用域中和var表现都有区别。
- 在 for-in 与 for-of 循环中, let 与 const 都能在每一次迭代时创建一个新的绑定,这意味着在循 环体内创建的函数可以使用当前迭代所绑定的循环变量值(而不是像使用 var 那样,统一使 用循环结束时的变量值)。这一点在 for 循环中使用 let 声明时也成立,不过在 for 循 环中使用 const 声明则会导致错误。
- 在全局作用域上使用 let 或 const声明变量 ,虽在全局作用域上会创建新的绑定,但不 会有任何属性被添加到全局对象window上。
而var在全局作用域上声明的变量会挂载到window上,成为window的一个属性
网友评论