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
時,當程式解析到這兩個特殊函式時,並不是直接將相應的任務(可以理解為回撥函式程式碼)放入到非同步任務佇列中,而是在相應的時間之後,將非同步任務放入到非同步任務佇列中。
所以,前文迴圈中的setTimeou
t我們可以理解會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
迴圈閉包系列問題的思考,文中如有紕漏, 歡迎大家指正,一同交流學習。