有關(guān)循環(huán)+閉包的一個(gè)代碼片段

問題背景


原始代碼
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,如圖:

image.png


為何會(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é)果如下,可以看到12是最后才被輸出的,而非在beforeafter中間被輸出:

image.png


改進(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é)果:

image.png

可以看到,這兩種方案都可以解決我們的問題!
我們?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é)果:

image.png

只是把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è)變量。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容