1. 程式人生 > >setTimeout與迴圈閉包

setTimeout與迴圈閉包

文章目錄

情景

今天在複習JS的閉包相關的知識時,看到有一篇文章中講到了setTimeout/setInterval

的閉包,不禁勾起我之前對這個問題的一系列想法,下面針對這個問題進行詳細的講解。以setTimeout為列,示列程式碼如下:

for(var i = 0; i < 4 ; i++){
	setTimeout(function(){
		console.log(i)
	}, i * 1000);
}

一般情況下,你可能會認為這段程式碼會依次輸出如下內容
0, 1, 2, 3
然而事實是,會每隔一秒輸出如下內容(請自行將逗號腦補為換行)
4, 4, 4, 4
要理解程式為什麼會有這樣的輸出,我覺得有兩個重要的概念必須理解,這兩個重要的概念就是JS的事件機制和閉包下面我分開講講這兩個概念。

事件機制

對於這個事件機制,我在本文就進行大概的講解,具體的講解後面會出專門的部落格來進行講解。

1. 單執行緒

首先JS是單執行緒的單執行緒的,也就是一個時刻只會有一個進行執行,而執行的程序是來自一個類似執行棧中的任務。

2. 任務型別

JS的任務分為同步任務和非同步任務

3. 任務執行流程

在進行JS程式碼解析時,同步任務的程式碼會根據程式的先後順序進入執行棧, 非同步任務則會在適當的時候進入非同步任務佇列, 當執行棧中的任務執行完了之後,就會去非同步任務佇列中獲取任務放入執行棧執行。為什麼說非同步任務會在適當的時候進入非同步任務佇列呢,取個列子,當我們使用setTimeout

setInterval時,當程式解析到這兩個特殊函式時,並不是直接將相應的任務(可以理解為回撥函式程式碼)放入到非同步任務佇列中,而是在相應的時間之後,將非同步任務放入到非同步任務佇列中。

所以,前文迴圈中的setTimeout我們可以理解會4個不同的非同步任務,每個任務的主體內容都是一樣的,都是回撥函式。而setTimeout外面的for迴圈,屬於同步任務,在其執行完以後,才會執行我們的setTimeout的回撥函式。下面我們再看看setTimeout的回撥函式。

閉包

當然,閉包這麼高深的東西本篇博文也是不打算詳細講解的,後續還是會單獨對這個高深的概率進行詳細探討的。這裡只是針對上文的程式碼進行簡單的講解。

1. 閉包的定義

首先,閉包最簡單的理解就是在函式中定義函式,同時在定義的函式內部呼叫了函式外部的變數時,便形成了閉包。
我們仔細看看程式碼

for(var i = 0; i < 4 ; i++){
	setTimeout(function(){
		console.log(i)
	}, i * 1000);
}
2. 實列中的閉包

我們可以看到在回撥函式中,呼叫了for迴圈中變數i,而這個變數i並不是匿名回撥函式的變數。所以這個匿名函式實際上就形成了一個閉包,閉包中的i依賴於外部迴圈中的i,而這4個setTimeout的回撥函式都是依賴這同一個i,就是迴圈結束時候的i,所以最後的輸出結果都是4。

解決方案

1. 使用es6的let
```
for(let i = 0; i < 4; i++){
	setTimeout(function(){
		console.log(i)
	}, i*1000)
}
```
2. 使用立即執行函式
for (var i = 0 ; i <=4 ; i++) {
      setTimeout( (function(res){
        return function(){
          console.log(res);
        };
      })(i) , i * 1000);
}

這種方法就是利用立即執行函式和閉包,將迴圈體執行中的i最為引數傳遞給內部的函式,而內部的函式return出來的結果是一個函式,這個函式呼叫了內部函式的變數res, 形成了閉包,所以這個res就會被儲存下來,等待函式呼叫的時候執行。

3. 使用setTimeout的第三個引數
for(var i = 0 ; i < 5; i++){
      setTimeout( function(t){
        console.log(t)
      },i * 1000, i);
}

這種方法就是利用了setTimeout函式的第三個引數,它可以作為引數傳遞給回撥函式。

總結

以上是個人對於這個setTimeout迴圈閉包系列問題的思考,文中如有紕漏, 歡迎大家指正,一同交流學習。