案例一
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000)
}
输出结果:
- 当时间是固定的数,如0、1000、6000,执行结果就是0、1、6秒后,一次输出五个6;
- 当时间是 i*1000, 输出是:每隔1秒,输出一个6,共5次。
代码中到底有什么‘缺陷’,导致它的行为与 语义暗示的不一致呢?
缺陷 是 我们试图假设
循环中的每一个迭代在运行时,都会给自己’捕获’一个i的副本。但是,根据作用域的工作原理,实际情况是,尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。这样说的话,当然所以函数共享一个 i 的引用。
循环结构让我们误以为背后还有更复杂的机制在起作用,实际上并没有。如果将延迟函数的回调重复定义5次,完全不使用循环,那它同这段代码是完全等价的。
----《你不知道的JS 上卷》p49
show me the code
第一种情况
setTimeout()
第二个参数是个常数
书中那段话就是说,
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, 1000)
}
等价于
// 注:执行栈内循环需要先结束
var i = 1;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 2;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 3;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 4;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 5;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 6;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
// 循环结束后,线程读取任务列表,将定时事件对应的异步任务(回调函数)入栈执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
输出情况:
当时间是固定的数,如0、1000、6000,执行结果就是0、1、6秒后,一次输出五个6;
等到这些回调执行时,i的值就是6
第二种情况
setTimeout()
第二个参数含有变量
同上
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000)
}
等价于
var i = 1;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 2;
// 定时器将其回调函数加入任务列表,执行栈清空二秒后执行
var i = 3;
// 定时器将其回调函数加入任务列表,执行栈清空三秒后执行
var i = 4;
// 定时器将其回调函数加入任务列表,执行栈清空四秒后执行
var i = 5;
// 定时器将其回调函数加入任务列表,执行栈清空五秒后执行
var i = 6;
// 定时器将其回调函数加入任务列表,执行栈清空六秒后执行
// 循环结束后,线程读取任务列表,将定时事件对应的异步任务(回调函数)入栈执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
输出情况:
当时间是 i*1000, 输出是:每隔1秒,输出一个6,共5次。
因为等到这些回调执行时,i的值就是6
HOW TO FIX IT
我知道,闭包!
for (var i = 1; i <= 5; i++) {
(function() {
setTimeout( function timer() {
console.log(i);
}, i*1000)
})()
}
然并卵,还是一秒一个6,五次
因为,新加上的 IIFE(创建并立即执行) 作用域是”空的”,它并没有自己的变量。执行栈清空后,线程从任务队列里读取回调函数,它们还是引用那个唯一的全局变量i。
正确的闭包姿势:
通过在闭包作用域中添加自己的变量,从而在每次迭代中,捕获i的副本。
for (var i = 1; i <= 5; i++) {
(function() {
var j = i
setTimeout( function timer() {
console.log(j);
}, j*1000) //至于时间这里,是i 是j无所谓
})()
}
更简洁的姿势:
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log(j);
}, j*1000)
})(i)
}
由此,能够输出:1 2 3 4 5,一秒一个
ES6的打开方式: 块作用域
for (var i = 1; i <= 5; i++) {
let j = i
setTimeout( function timer() {
console.log(j);
}, j*1000)
}
或直接在for循环头部里,每次迭代都声明一次
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000)
}
案例2
《JavaScript高级程序设计第三版》 p181
var a = function ceateFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i
}
}
return result
}
console.log(a()[0]());
//0 - 9 输出都是10
道理一样。
都是因为引用同一个i,且没能在迭代中捕获i的副本,或者没能在迭代中及时按当时的值执行。直到i早都变成10了,才执行,RHS引用的结果当然是i此刻的值,即10。
闭包处理:
var a = function ceateFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
// 创建匿名函数,并立即执行之,将执行结果赋值给数组
result[i] = (function(num) {
return function() {
return num;
};
})(i);
}
return result;
};
console.log(a()[0]());