blog
blog copied to clipboard
作用域和Closure ②
作用域和Closure ②
第一节,理解了closure的含义之后,我们继续探讨 scope 和 closure 的关系。
编写代码:closure 与 scope
过去,不了解JS中的 closure,当你在for循环中调用函数的时候,就很容易发生意想不到的结果。
来看下面这个例子:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000 );
}
如果你执行下就会发现,得到 5个 数字6。
- 首先,6是for循环终止后i的值。并且,即便我们给setTimeout的第二个参数传递0,它的回调函数timer也同样会在for循环结束之后才执行
- 其次,for 循环在JS中是没有自己作用域的。因此,循环变量i实际上是定义在全局作用域的变量
- 第三,每次循环,timer通过 closure 捕获的都是同一个全局变量 i
所以最终打印出来的值 是同一个全局变量 i 的 值:6
解决方案
那么如何解决这个看似复杂的问题呢?
其实,最关键的一点就是,不要让所有的 timer 都捕获同一个变量 i,而是让它们捕获当前迭代时 i 的一个拷贝。有了这个思路后,我们一步步来实现它。
IIFE 方案
我们让timer在每一次迭代的时候,捕获自己的作用域,而不是捕获全局作用域,这可以通过 IIFE (立即执行函数)实现:
for (var i = 1; i <= 10; i++) {
(function() {
// Scope begin
setTimeout(function timer() {
console.log(i);
}, 0);
// Scope close
})();
}
这样,每一次迭代,timer 捕获到的,就是 // Scope begin 和 // Scope close 之间的作用域了。但这还不够,因为JS不会捕获一个空的作用域。
所以,我们在 timer 捕获的作用域里,保存一份当前 i 的拷贝:
for (var i = 1; i <= 5; i++) {
(function() {
// Scope begin
var j = i;
setTimeout(function timer() {
console.log(j);
}, 0);
// Scope close
})();
}
现在重新执行一下,就可以看到一开始期待的 12345 了。当然,我们也可以把上面的代码写成这样:
for (var i = 1; i <= 5; i++) {
(function(j) {
// Scope begin
setTimeout(function timer() {
console.log(j);
}, 0);
// Scope close
})(i);
}
道理是一样的,函数的参数,实际上也是一个函数的临时变量。
ES6方案
上面的方案,有种为了解决普世问题而引发了不必要的代码。
在ECMAScript 6里,引入了一种在块作用域(block scope)中定义变量的方法:
for (var i = 1; i <= 5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, 0);
}
用let定义的变量,更符合我们在其他编程语种的直觉,每一次迭代,都会有一个全新的变量 j,这样,timer 每次捕获的,就是当前 i 的拷贝了。
当然,既然 let 定义的变量有这个特性,我们可以直接用它来定义for循环,这就和我们印象中的for几乎一样了:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
重新执行一下,结果仍旧是 12345