問題背景
原始代碼
for (var i=1; i<6; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
預(yù)期效果
分別輸出數(shù)字1~5、每秒1次、每次1個(gè)
實(shí)際效果
1以每秒1次的頻率輸出了5個(gè)6,如圖:

為何會(huì)產(chǎn)生和語義不符的預(yù)期?
首先解釋“6”從何而來
上述代碼中,循環(huán)的終止條件是i不再<6,條件首次成立時(shí)i的值是6.因此,輸出顯示的是循環(huán)結(jié)束時(shí)i的最終值。
上述代碼的缺陷
這里的缺陷是,我們假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己捕獲一個(gè)i的副本。但是根據(jù)作用域的工作原理,雖然i是在5次迭代中分別定義的,但是它們都會(huì)被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè)i。
延遲函數(shù)的回調(diào)
需要注意的是,延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上,當(dāng)定時(shí)器運(yùn)行時(shí),即使每個(gè)迭代中執(zhí)行的是setTimeout(..., 0),所有的回調(diào)函數(shù)仍然是在循環(huán)結(jié)束后才被執(zhí)行。(因?yàn)?code>setTimeout實(shí)際執(zhí)行是在線程最后的,首先執(zhí)行的是所有的同步代碼。)
舉個(gè)例子:
for (let i=1; i<3; i++) {
console.log('before');
setTimeout(() => {
console.log(i);
}, i*1000);
console.log('after');
}
這段代碼的運(yùn)行結(jié)果如下,可以看到1和2是最后才被輸出的,而非在before和after中間被輸出:

改進(jìn)
思路
我們?cè)谘h(huán)過程的每個(gè)迭代中都需要一個(gè)閉包作用域,從而可以保存不同的i值。
改進(jìn)1:無效的嘗試
for (var i=1; i<6; i++) {
(function() {
setTimeout(function timer() {
console.log(i);
}, i*1000);
})();
}
此方案的運(yùn)行結(jié)果仍是錯(cuò)誤的。在這里我們?cè)噲D借助一個(gè)立即執(zhí)行的匿名函數(shù)(IIFE)來創(chuàng)建單獨(dú)的詞法作用域,但此時(shí)這個(gè)作用域是空的,因此不會(huì)起作用。它需要包含一點(diǎn)實(shí)質(zhì)內(nèi)容。
改進(jìn)2:對(duì)IIFE方案的改進(jìn)
for (var i=1; i<6; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
})();
}
或者:
for(var i=1; i<6; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000)
})(i);
}
運(yùn)行結(jié)果:

可以看到,這兩種方案都可以解決我們的問題!
我們?cè)诿看蔚淖饔糜蛑新暶髁诵碌淖兞?code>j(或者隨便叫什么名字),使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個(gè)迭代內(nèi)部,每個(gè)迭代中都會(huì)含有一個(gè)具有正確值的變量供我們?cè)L問。
改進(jìn)3:使用let
for (let i=1; i<6; i++) {
setTimeout(() => {
console.log(i);
}, i*1000);
}
運(yùn)行結(jié)果:

只是把
var i=1改為let i=1,就可以得到正確的結(jié)果!這是因?yàn)椋?code>for循環(huán)頭部的
let聲明會(huì)有一個(gè)特殊的行為,這個(gè)行為指出變量在循環(huán)過程中不止被聲明一次,每次迭代都會(huì)聲明。隨后的每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來初始化這個(gè)變量。