[譯]JS閉包:For循環中的setTimeout

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

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

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循環裏實際發生了什麼: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

若是咱們想在循環中每秒輸出一次應該怎麼作呢?咱們會想固然的認爲只要添加一個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運行在瀏覽器中,瀏覽器作了不少事情不只僅是執行代碼這麼簡單。事實上,瀏覽器須要考慮這四個部分:oop

  • JavaScript運行時引擎
  • 瀏覽器提供的Web APIs,好比DOM,setTimeout等等
  • 具備回調函數(如onClick和onLoad)的事件的回調隊列
  • 事件循環

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

運行引擎執行咱們的代碼,每一個瀏覽器都有一個稍微不一樣的引擎。例如,Chrome使用V8引擎,這也剛好爲NodeJs提供支持。該引擎一次只能執行一段代碼。
Web APIs是瀏覽器提供給咱們的,其中包含了像setTimeout()這種方法。若是你在瀏覽器的控制檯把window打印出來,你會看到一個很長很長的默認的API列表。

這些API是由瀏覽器在一個單獨的進程裏獨立運行的。 這就是JavaScript能夠發生異步的緣由!!! 並非JavaScript自己能夠同時作多件事;而是,瀏覽器能夠同時爲咱們運行多個不一樣的進程。 到這裏我但願你提出的問題是,運行引擎和Web API 是怎麼樣相互協同工做的?答案是經過 回調隊列事件循環
回調隊列它是須要在JavaScript運行引擎中斷後執行的一個任務隊列。
事件循環是最後一個須要破解的謎團,它是一個不斷運行的循環,用來鏈接堆棧和回調隊列。 接下來讓咱們看一下它們在咱們 for循環和 setTimeout的例子中是如何一塊兒運行工做的。

閉包

setTimeout能夠經過閉包拿到i的值。咱們把i放在console.log語句裏,可是i的值卻被設置在外面一層的封閉範圍內,即for循環裏。既然內部函數能夠拿到外部函數的變量,咱們就能去for循環裏取到i的值,即5。

相關文章
相關標籤/搜索