談談 setTimeout 這道經典題目

談談本身對下面這道題目的理解html

問題

for (var i = 1; i <= 3; i++) { 
    setTimeout( function timer() {
        console.log(i);
    }, i * 1000 );
}

這段代碼的輸出是三次 4,與預想的 1,2,3 的輸出不符。如下解釋這一輸出的緣由。前端

分析

咱們能夠將 setTimeout 的第一個參數 timer() 單獨寫出來,變成以下代碼:segmentfault

for (var i = 1; i <= 3; i++) { 
    function timer() {
        console.log(i);
    }
    setTimeout( timer, i * 1000 );
}

而後咱們將循環展開,三次執行過程的變化以下:瀏覽器

// 第一步: i = 1;
setTimeout( timer, 1 * 1000 );

// 第二步:i = 2;
setTimeout( timer, 2 * 1000 );

// 第三步 i = 3;
setTimeout( timer, 3 * 1000 );

注意,在循環過程當中,timer() 函數並未變化,也沒有執行( 計時器還未開始 )。閉包

因爲 JavaScript 中使用 var i = xxx 聲明的變量是函數級別( 而非塊級 )的做用域,於是在 for 循環條件中聲明的 i 在 for 循環塊以外的最後一個函數體內還是能夠訪問的,循環能夠展開爲:函數

var i = 4;
function timer() {
    console.log(i);
}
setTimeout( timer, 1 * 1000 );
setTimeout( timer, 2 * 1000 );
setTimeout( timer, 3 * 1000 );

於是當計時器開始的 1s, 2s, 3s 後,timer 會分別執行,此時會輸出三次 4。測試

解決方法

若要其每隔 1s 分別輸出 1, 2, 3,能夠將 var i = 1 修改成 let i = 1,即:code

for (let i = 1; i <= 3; i++) { 
    function timer() {
        console.log(i);
    }
    setTimeout( timer, i * 1000 );
}

注意,因爲 let 屬於 ES6 的語法,請注意測試使用的瀏覽器。htm

此時,因爲 let i = xxx 爲塊級別做用域,於是這一狀況下的循環展開結果爲:blog

{
    let i = 1;
    setTimeout( timer, 1 * 1000 );
}
{
    let i = 2;
    setTimeout( timer, 2 * 1000 );
}
{
    let i = 3;
    setTimeout( timer, 3 * 1000 );
}

注意:這裏的 {} 僅用來強調塊級別做用域。

此時即可以獲得咱們想要的輸出結果了。

此外,還能夠使用下面這種方式:

for (var i = 1; i <= 3; i++) { 
    (function(count){
        setTimeout( function timer() {
            console.log(count);
        }, count * 1000 );
    })(i)
}

這裏能夠使用閉包的知識進行解釋( 有關閉包的內容能夠參見文末的參考連接 ),也能夠用做用域輔助理解。

因爲 var i = xxx 是函數級別做用域,這裏經過一個當即函數將變量 i 傳入其中,使其包含在這一函數的做用域中。而在每次循環中,此當即函數都會將傳入的 i 值保存下來,於是其循環展開結果爲:

(function(){
    var count = 1;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 2;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()
(function(){
    var count = 3;
    setTimeout( function timer() {
        console.log(count);
    }, count * 1000 );
})()

天然也會獲得咱們想要的輸出結果。

擴展 - 塊級做用域和函數級做用域

能夠用如下代碼進行解釋:

{
    let i = 2;
    // 輸出 2
    console.log(i);
}
// 報錯:Uncaught ReferenceError: i is not defined
console.log(i);
function test(){
    // 因爲變量提高,輸出 undefined
    console.log(a);
    {
        var a = 1;
    }
    // 輸出 1
    console.log(a);
}
// 按照函數內的註釋輸出
test();
// 報錯:Uncaught ReferenceError: a is not defined
console.log(a);

注:const 聲明的常量與 let 相同,也爲塊級做用域。


參考

  1. for 循環中的...問題,爲何改 var 爲 let 就能夠解決? - segmentfault

  2. ES6之let(理解閉包)和const命令 - 博客園

  3. 「每日一題」JS 中的閉包是什麼? - 知乎專欄

  4. 前端基礎進階(四):詳細圖解做用域鏈與閉包 - 簡書

相關文章
相關標籤/搜索