譯者:嘴裏起了個泡
原文地址: wsvincent.com/javascript-…javascript
這篇文章詳細介紹了JS在執行for循環裏面的 setTimeout()
語句的時候發什麼了什麼。這是面試中常常會被問到的一個問題,由於這個問題的答案涉及到了幾個JS的核心知識點:閉包(closures),提高(hoisting)和事件循環(the event loop)。java
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
循環裏實際發生了什麼:bash
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
方法就能達到這個效果。併發
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是單線程單一併發語言,這意味着它一次只能處理一個任務或一段代碼。讓咱們接着看: 因此咱們如何用它寫出異步的代碼呢,就好比上面例子中的setTimeout()
? 答案是JavaScript運行在瀏覽器中,瀏覽器作了不少事情不只僅是執行代碼這麼簡單。事實上,瀏覽器須要考慮這四個部分:oop
DOM
,setTimeout
等等下面這個圖片來自Philip Roberts’s fantastic talk on the Event Loop視頻裏的截圖:
運行引擎執行咱們的代碼,每一個瀏覽器都有一個稍微不一樣的引擎。例如,Chrome使用V8引擎,這也剛好爲NodeJs提供支持。該引擎一次只能執行一段代碼。
Web APIs是瀏覽器提供給咱們的,其中包含了像setTimeout()
這種方法。若是你在瀏覽器的控制檯把window
打印出來,你會看到一個很長很長的默認的API列表。
for
循環和
setTimeout
的例子中是如何一塊兒運行工做的。
setTimeout
能夠經過閉包拿到i
的值。咱們把i
放在console.log
語句裏,可是i
的值卻被設置在外面一層的封閉範圍內,即for
循環裏。既然內部函數能夠拿到外部函數的變量,咱們就能去for
循環裏取到i
的值,即5。