譯者:嘴裡起了個泡
原文地址: 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視訊裡的截圖:
執行引擎執行我們的程式碼,每個瀏覽器都有一個稍微不同的引擎。例如,Chrome使用V8引擎,這也恰好為NodeJs提供支援。該引擎一次只能執行一段程式碼。
Web APIs是瀏覽器提供給我們的,其中包含了像setTimeout()
這種方法。如果你在瀏覽器的控制檯把window
列印出來,你會看到一個很長很長的預設的API列表。
回撥佇列它是需要在JavaScript執行引擎中斷後執行的一個任務佇列。
事件迴圈是最後一個需要破解的謎團,它是一個不斷執行的迴圈,用來連線堆疊和回撥佇列。 接下來讓我們看一下它們在我們
for
迴圈和setTimeout
的例子中是如何一起執行工作的。
閉包
setTimeout
可以通過閉包拿到i
的值。我們把i
放在console.log
語句裡,但是i
的值卻被設定在外面一層的封閉範圍內,即for
迴圈裡。既然內部函式可以拿到外部函式的變數,我們就能去for
迴圈裡取到i
的值,即5。