我在詳細圖解作用域鏈與閉包一文中的結尾留下了一個關於setTimeout與迴圈閉包的思考題。
利用閉包,修改下面的程式碼,讓迴圈輸出的結果依次為1, 2, 3, 4, 5
12345 for (var i=1; i<=5; i++) {setTimeout( function timer() {console.log(i);}, i*1000 );}
值得高興的是很多朋友在讀了文章之後確實對閉包有了更加深刻的瞭解,並準確的給出了幾種寫法。一些朋友能夠認真的閱讀我的文章並且一個例子一個例子的上手練習,這種認可對我而言真的非常感動。但是也有一些基礎稍差的朋友在閱讀了之後,對於這題的理解仍然感到困惑,因此應一些讀者老爺的要求,藉此文章專門對setTimeout進行一個相關的知識分享,願大家讀完之後都能夠有新的收穫。
在最初學習setTimeout的時候,我們很容易知道setTimeout有兩個引數,第一個引數為一個函式,我們通過該函式定義將要執行的操作。第二個引數為一個時間毫秒數,表示延遲執行的時間。
1 2 3 |
setTimeout(function() { console.log('一秒鐘之後我將被列印出來') }, 1000) |
可能不少人對於setTimeout的理解止步於此,但還是有不少人發現了一些其他的東西,並在評論裡提出了疑問。比如上圖中的這個數字7,是什麼?
每一個setTimeout在執行時,會返回一個唯一ID,上圖中的數字7,就是這個唯一ID。我們在使用時,常常會使用一個變數將這個唯一ID儲存起來,用以傳入clearTimeout,清除定時器。
1 2 3 4 5 |
var timer = setTimeout(function() { console.log('如果不清除我,我將會一秒之後出現。'); }, 1000) clearTimeout(timer); // 清除之後,通過setTimeout定義的操作並不會執行 |
接下來,我們還需要考慮另外一個重要的問題,那就是setTimeout中定義的操作,在什麼時候執行?為了引起大家的重視,我們來看看下面的例子。
1 2 3 4 5 6 7 |
var timer = setTimeout(function() { console.log('setTimeout actions.'); }, 0); console.log('other actions.'); // 思考一下,當我將setTimeout的延遲時間設定為0時,上面的執行順序會是什麼? |
在瀏覽器中的console中執行試試看,很容易就能夠知道答案,如果你沒有猜中答案,那麼我這篇文章就值得你點一個讚了,因為接下來我分享的小知識,可能會在筆試中救你一命。
在對於執行上下文的介紹中,我與大家分享了函式呼叫棧這種特殊資料結構的呼叫特性。在這裡,將會介紹另外一個特殊的佇列結構,頁面中所有由setTimeout定義的操作,都將放在同一個佇列中依次執行。
我用下圖跟大家展示一下佇列資料結構的特點。
而這個佇列執行的時間,需要等待到函式呼叫棧清空之後才開始執行。即所有可執行程式碼執行完畢之後,才會開始執行由setTimeout定義的操作。而這些操作進入佇列的順序,則由設定的延遲時間來決定。
因此在上面這個例子中,即使我們將延遲時間設定為0,它定義的操作仍然需要等待所有程式碼執行完畢之後才開始執行。這裡的延遲時間,並非相對於setTimeout執行這一刻,而是相對於其他程式碼執行完畢這一刻。所以上面的例子執行結果就非常容易理解了。
為了幫助大家理解,再來一個結合變數提升的更加複雜的例子。如果你能夠正確看出執行順序,那麼你對於函式的執行就有了比較正確的認識了,如果還不能,就回過頭去看看其他幾篇文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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就暫時先介紹到這裡,我們回過頭來看看那個迴圈閉包的思考題。
1 2 3 4 5 |
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值並儲存在閉包中。
1 2 3 4 5 6 7 8 |
for (var i=1; i<=5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i*1000 ); })(i) } |
當然,也可以在setTimeout的第一個引數處利用閉包。
1 2 3 4 5 6 7 |
for (var i=1; i<=5; i++) { setTimeout( (function(i) { return function() { console.log(i); } })(i), i*1000 ); } |