附件1:setTimeout與閉包

我在詳細圖解做用域鏈與閉包一文中的結尾留下了一個關於setTimeout與循環閉包的思考題。html

利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5chrome

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

值得高興的是不少朋友在讀了文章以後確實對閉包有了更加深入的瞭解,並準確的給出了幾種寫法。一些朋友可以認真的閱讀個人文章而且一個例子一個例子的上手練習,這種承認對我而言真的很是感動。可是也有一些基礎稍差的朋友在閱讀了以後,對於這題的理解仍然感到困惑,所以應一些讀者老爺的要求,藉此文章專門對setTimeout進行一個相關的知識分享,願你們讀完以後都可以有新的收穫。瀏覽器

在最初學習setTimeout的時候,咱們很容易知道setTimeout有兩個參數,第一個參數爲一個函數,咱們經過該函數定義將要執行的操做。第二個參數爲一個時間毫秒數,表示延遲執行的時間。數據結構

setTimeout(function() {
    console.log('一秒鐘以後我將被打印出來')
}, 1000)

上例執行結果

可能很多人對於setTimeout的理解止步於此,但仍是有很多人發現了一些其餘的東西,並在評論裏提出了疑問。好比上圖中的這個數字7,是什麼?閉包

每個setTimeout在執行時,會返回一個惟一ID,上圖中的數字7,就是這個惟一ID。咱們在使用時,經常會使用一個變量將這個惟一ID保存起來,用以傳入clearTimeout,清除定時器。函數

var timer = setTimeout(function() {
    console.log('若是不清除我,我將會一秒以後出現。');
}, 1000)

clearTimeout(timer);  // 清除以後,經過setTimeout定義的操做並不會執行

接下來,咱們還須要考慮另一個重要的問題,那就是setTimeout中定義的操做,在何時執行?爲了引發你們的重視,咱們來看看下面的例子。學習

var timer = setTimeout(function() {
    console.log('setTimeout actions.');
}, 0);

console.log('other actions.');

// 思考一下,當我將setTimeout的延遲時間設置爲0時,上面的執行順序會是什麼?

在瀏覽器中的console中運行試試看,很容易就可以知道答案,若是你沒有猜中答案,那麼我這篇文章就值得你點一個讚了,由於接下來我分享的小知識,可能會在筆試中救你一命。3d

在對於執行上下文的介紹中,我與你們分享了函數調用棧這種特殊數據結構的調用特性。在這裏,將會介紹另一個特殊的隊列結構,頁面中全部由setTimeout定義的操做,都將放在同一個隊列中依次執行。調試

我用下圖跟你們展現一下隊列數據結構的特色。code

隊列:先進先出

而這個隊列執行的時間,須要等待到函數調用棧清空以後纔開始執行。即全部可執行代碼執行完畢以後,纔會開始執行由setTimeout定義的操做。而這些操做進入隊列的順序,則由設定的延遲時間來決定。

所以在上面這個例子中,即便咱們將延遲時間設置爲0,它定義的操做仍然須要等待全部代碼執行完畢以後纔開始執行。這裏的延遲時間,並不是相對於setTimeout執行這一刻,而是相對於其餘代碼執行完畢這一刻。因此上面的例子執行結果就很是容易理解了。

爲了幫助你們理解,再來一個結合變量提高的更加複雜的例子。若是你可以正確看出執行順序,那麼你對於函數的執行就有了比較正確的認識了,若是還不能,就回過頭去看看其餘幾篇文章。

setTimeout(function() {
    console.log(a);
}, 0);

var a = 10;

console.log(b);
console.log(fn);

var b = 20;

function fn() {
    setTimeout(function() {
        console.log('setTImeout 10ms.');
    }, 10);
}

fn.toString = function() {
    return 30;
}

console.log(fn);

setTimeout(function() {
    console.log('setTimeout 20ms.');
}, 20);

fn();

上慄執行結果

OK,關於setTimeout就暫時先介紹到這裏,咱們回過頭來看看那個循環閉包的思考題。

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

若是咱們直接這樣寫,根據setTimeout定義的操做在函數調用棧清空以後纔會執行的特色,for循環裏定義了5個setTimeout操做。而當這些操做開始執行時,for循環的i值,已經先一步變成了6。所以輸出結果總爲6。而咱們想要讓輸出結果依次執行,咱們就必須藉助閉包的特性,每次循環時,將i值保存在一個閉包中,當setTimeout中定義的操做執行時,則訪問對應閉包保存的i值便可。

而咱們知道在函數中閉包斷定的準則,即執行時是否在內部定義的函數中訪問了上層做用域的變量。所以咱們須要包裹一層自執行函數爲閉包的造成提供條件。

所以,咱們只須要2個操做就能夠完成題目需求,一是使用自執行函數提供閉包條件,二是傳入i值並保存在閉包中。

for (var i=1; i<=5; i++) {

    (function(i) {
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    })(i)
}

利用斷點調試,在chrome中查看執行順序與每個閉包中不一樣的i值

固然,也能夠在setTimeout的第一個參數處利用閉包。

for (var i=1; i<=5; i++) {
    setTimeout( (function(i) {
        return function() {
            console.log(i);
        }
    })(i), i*1000 );
}
相關文章
相關標籤/搜索