直接上代码例子
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000 );
}
我们会在控制台看到期望的12345?如果你执行下就会发现并不会,而是会得到66666。
6是哪来的呢?为了回答这个问题,
首先,6是for
循环终止后i
的值,即便我们给setTimeout
的第二个参数传递0,它的回调函数timer
也同样会在for
循环结束之后才执行;
其次,for
循环在JS中是没有自己作用域的。因此,循环变量i
实际上是定义在全局作用域的变量;
第三,每次循环,timer
通过closure捕获的都是同一个全局变量i。
思路:
不要让所有的timer
都捕获同一个变量i
,而是让它们捕获当前迭代时i
的一个拷贝。
首先,让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里,引入了一种在块作用域(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++) { //每次循环都会声明一个新的i,并且赋值为旧的i+1。
setTimeout(function timer() {
console.log(i);
}, 0);
}
可以看做是 let和var的区别之一。