[譯]JS閉包:For迴圈中的setTimeout

嘴裡起了個泡發表於2019-04-15

譯者:嘴裡起了個泡
原文地址: wsvincent.com/javascript-…

這篇文章詳細介紹了JS在執行for迴圈裡面的 setTimeout() 語句的時候發什麼了什麼。這是面試中經常會被問到的一個問題,因為這個問題的答案涉及到了幾個JS的核心知識點:閉包(closures)提升(hoisting)事件迴圈(the event loop)

For迴圈

For迴圈是JS開發中經常使用的。它會一直執行直到其中的判斷條件為false。一個For迴圈包含三個分句:一個初始化表示式,一個條件表示式和一個更新表示式。

for (var i = 1; i < 5; i++) {
  console.log(i);  // 1 2 3 4
}
複製程式碼

現在我們的三個分句如下:

  • 初始化:var i = 1
  • 條件: i < 5
  • 更新:i++

需要注意的是在這個for迴圈結束的時候,變數i的值實際上是5,不是4。我們從初始化開始,每次i遞增1,然後檢查i是否滿足條件。換句話說,我們會按照1,3,2的順序執行這三個分句,儘管邏輯上會認為它們應該按順序執行。
讓我們來檢查一下for迴圈裡實際發生了什麼:

  • 第一步:i值為1,增加到2,檢查2 < 5?滿足條件,所以列印輸出。
  • 第二步:i值為2,增加到3,檢查3 < 5?滿足條件,所以列印輸出。
  • 第三步:i值為3,增加到4,檢查4 < 5?滿足條件,所以列印輸出。
  • 第四步:i值為4,增加到5,檢查5 < 5?不滿足條件,終止迴圈。
    現在我們清楚了,為什麼i最終等於5,但是卻只列印出來了1-4。我們可以通過下面程式碼來證明這點。
for (var i = 1; i < 5; i++) {
  console.log(i); // 1 2 3 4
}

console.log("The value of i is now: ", i); // "The value of i is now: 5"
複製程式碼

閉包

關鍵字Var的作用域是函式範圍內,意味著它位於一個封閉的函式中。但我們上面的例子中並沒有函式,所以它的作用域就是全域性。也就是說,上面的for迴圈建立了一個全域性變數i
請注意,既然var的作用域是函式範圍內,那麼i的作用域就會被設定到離它最近的函式中。在這個例子中,它將會是全域性變數。

setTimeout

如果我們想在迴圈中每秒輸出一次應該怎麼做呢?我們會想當然的認為只要新增一個setTimeout方法就能達到這個效果。

for (var i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 1000)  // 5 5 5 5
}
複製程式碼

事與願違!!!為什麼沒有輸出1 2 3 4呢?在這個微妙的例子中實際上發生了很多事情。
簡單的回答就是for迴圈先執行掉了,然後再去尋找i的值,發現是5,然後把它列印了四次,每個迴圈列印一次。
即使我們把迴圈的時間間隔設定成0,結果還是一樣的。

for (var i = 1; i < 5; i++) {
    setTimeout(() => console.log(i), 0)  // 5 5 5 5
}
複製程式碼

我相信你對此肯定很疑惑。不用擔心:你很快就會知道這到底是怎麼回事了。

JavaScript 執行引擎

JavaScript是單執行緒單一併發語言,這意味著它一次只能處理一個任務或一段程式碼。讓我們接著看: 所以我們如何用它寫出非同步的程式碼呢,就比如上面例子中的setTimeout()? 答案是JavaScript執行在瀏覽器中,瀏覽器做了很多事情不僅僅是執行程式碼這麼簡單。事實上,瀏覽器需要考慮這四個部分:

  • JavaScript執行時引擎
  • 瀏覽器提供的Web APIs,比如DOM,setTimeout等等
  • 具有回撥函式(如onClick和onLoad)的事件的回撥佇列
  • 事件迴圈

下面這個圖片來自Philip Roberts’s fantastic talk on the Event Loop視訊裡的截圖:

[譯]JS閉包:For迴圈中的setTimeout

執行引擎執行我們的程式碼,每個瀏覽器都有一個稍微不同的引擎。例如,Chrome使用V8引擎,這也恰好為NodeJs提供支援。該引擎一次只能執行一段程式碼。
Web APIs是瀏覽器提供給我們的,其中包含了像setTimeout()這種方法。如果你在瀏覽器的控制檯把window列印出來,你會看到一個很長很長的預設的API列表。

[譯]JS閉包:For迴圈中的setTimeout
這些API是由瀏覽器在一個單獨的程式裡獨立執行的。這就是JavaScript可以發生非同步的原因!!! 並不是JavaScript本身可以同時做多件事;而是,瀏覽器可以同時為我們執行多個不同的程式。 到這裡我希望你提出的問題是,執行引擎和Web API 是怎麼樣相互協同工作的?答案是通過回撥佇列事件迴圈
回撥佇列它是需要在JavaScript執行引擎中斷後執行的一個任務佇列。
事件迴圈是最後一個需要破解的謎團,它是一個不斷執行的迴圈,用來連線堆疊和回撥佇列。 接下來讓我們看一下它們在我們for迴圈和setTimeout的例子中是如何一起執行工作的。

閉包

setTimeout可以通過閉包拿到i的值。我們把i放在console.log語句裡,但是i的值卻被設定在外面一層的封閉範圍內,即for迴圈裡。既然內部函式可以拿到外部函式的變數,我們就能去for迴圈裡取到i的值,即5。

相關文章