for循環 + setTimeout 結合的爛大街的面試題

1、背景

最近在翻看之前的老書《node.js開發指南》,剛好碰到 for 循環 + setTimeout 的經典例子,因而從新梳理了思路並記錄下。node

2、寫在前面,setTimeout 和 setInterval 的執行機制

在平常編碼中,你會發現,給 setTimeout 和 setInterval 設定延遲時間每每並不許,或者乾脆 setTimeout(function(){xxx},0) 也不是立馬執行(特別是有耗時代碼在前),這是由於 js 是單線程的,有一個事件隊列機制,setTimeout 和 setInterval 的回調會到了延遲時間塞入事件隊列中,排隊執行。異步

setTimeout :延時 delay 毫秒以後,啥也無論,直接將回調函數加入事件隊列。
setInterval :延時 delay 毫秒以後,先看看事件隊列中是否存在尚未執行的回調函數( setInterval 的回調函數),若是存在,就不要再往事件隊列里加入回調函數了。函數

看下面示例:編碼

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

結果:1 秒以後,同時輸出 5 個 5。線程

由於 for 循環會先執行完(同步優先於異步優先於回調),這時五個 setTimeout 的回調所有塞入了事件隊列中,而後 1 秒後一塊兒執行了。code

3、正文

接下來就是那道經典的代碼:隊列

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

結果:5 5 5 5 5事件

爲何不是 1 2 3 4 5,問題出在做用域上。作用域

由於 setTimeout 的 console.log(i); 的i是 var 定義的,因此是函數級的做用域,不屬於 for 循環體,屬於 global。等到 for 循環結束,i 已經等於 5 了,這個時候再執行 setTimeout 的五個回調函數(參考上面對事件機制的闡述),裏面的 console.log(i); 的 i 去向上找做用域,只能找到 global下 的 i,即 5。因此輸出都是 5。開發

解決辦法:人爲給 console.log(i); 創造做用域,保存i的值。

解決辦法一

for (var i = 0; i < 5; i++) { 
    (function(i){      //馬上執行函數
        setTimeout(function (){
            console.log(i);  
         },1000);  
    })(i);  
}

這裏用到馬上執行函數。這樣 console.log(i); 中的i就保存在每一次循環生成的馬上執行函數中的做用域裏了。

解決辦法二

for (let i = 0; i < 5; i++) {     //let 代替 var
    setTimeout(function (){
        console.log(i);  
     },1000);  
}

let 爲代碼塊的做用域,因此每一次 for 循環,console.log(i); 都引用到 for 代碼塊做用域下的i,由於這樣被引用,因此 for 循環結束後,這些做用域在 setTimeout 未執行前都不會被釋放。

4、補充

在寫示例代碼的過程當中,發現一個語法點:

function a(i){ 
    console.log(i);   
 }

for (var i = 0; i < 5; i++) { 
    setTimeout(a(i),1000); 
}

報錯:

TypeError: "callback" argument must be a function
at setTimeout (timers.js:421:11)
……

百度了下,原來 setTimeout 不支持傳帶參數的函數,有兩種解決方案:

(1)匿名函數包裝

function a(i){ 
    console.log(i);   
}

for (var i = 0; i < 5; i++) { 
    setTimeout(function(){  //用匿名函數包裝
        a(i);
    },1000); 
}

(2)setTimeout 的第 3+ 個參數

setTimeout(func, delay, param1, param2, ...)
第三個參數及之後的參數均可以做爲 func 函數的參數

function a(i){ 
    console.log(i);   
 }

for (var i = 0; i < 5; i++) { 
    setTimeout(a,1000,i);   //傳入第3個參數
}
相關文章
相關標籤/搜索