圖例詳解那道setTimeout與循環閉包的經典面試題
我在詳細圖解作用域鏈與閉包一文中的結尾留下了一個關于setTimeout與循環閉包的思考題。
利用閉包,修改下面的代碼,讓循環輸出的結果依次為1, 2, 3, 4, 5
- 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中運行試試看,很容易就能夠知道答案,如果你沒有猜中答案,那么我這篇文章就值得你點一個贊了,因為接下來我分享的小知識,可能會在筆試中救你一命。
在對于執行上下文的介紹中,我與大家分享了函數調用棧這種特殊數據結構的調用特性。在這里,將會介紹另外一個特殊的隊列結構,頁面中所有由setTimeout定義的操作,都將放在同一個隊列中依次執行。
我用下圖跟大家展示一下隊列數據結構的特點。
隊列:先進先出
而這個隊列執行的時間,需要等待到函數調用棧清空之后才開始執行。即所有可執行代碼執行完畢之后,才會開始執行由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 );
- }