setTimeout與循環(huán)閉包經(jīng)典面試題詳解
我在詳細(xì)圖解作用域鏈與閉包[1]一文中的結(jié)尾留下了一個(gè)關(guān)于setTimeout與循環(huán)閉包的思考題。
利用閉包,修改下面的代碼,讓循環(huán)輸出的結(jié)果依次為1, 2, 3, 4, 5
for (var i = 1; i <= 5; i++) {setTimeout(function timer() {console.log(i);}, i * 1000);}
值得高興的是,很多朋友在閱讀了我的文章之后確實(shí)對(duì)閉包有了更加深刻的了解,并準(zhǔn)確的給出了好幾種寫(xiě)法。大家能夠認(rèn)真的閱讀我的文章并且一個(gè)例子一個(gè)例子的上手練習(xí),這種認(rèn)可對(duì)我而言真的非常感動(dòng)。
但是也有一些基礎(chǔ)稍差的朋友在閱讀了之后,對(duì)于這題的理解仍然感到困惑,因此應(yīng)一些讀者老爺?shù)囊螅璐宋恼聦iT(mén)對(duì)setTimeout進(jìn)行一個(gè)相關(guān)的知識(shí)分享,希望大家讀完之后都能夠有新的收獲。
初學(xué)setTimeout,我們很容易知道setTimeout有兩個(gè)參數(shù),第一個(gè)參數(shù)為一個(gè)函數(shù),我們通過(guò)該函數(shù)定義將要執(zhí)行的操作。第二個(gè)參數(shù)為一個(gè)時(shí)間毫秒數(shù),表示延遲執(zhí)行的時(shí)間。
setTimeout(function() {console.log('一秒鐘之后我將被打印出來(lái)')}, 1000)
執(zhí)行結(jié)果如圖

可能不少同學(xué)對(duì)于setTimeout的理解止步于此,但還是有不少人發(fā)現(xiàn)了一些其他的東西,并在評(píng)論里提出了疑問(wèn)。比如上圖中的這個(gè)數(shù)字7,是什么?
每一個(gè)setTimeout在執(zhí)行時(shí),會(huì)返回一個(gè)唯一ID,上圖中的數(shù)字7,就是這個(gè)唯一ID。我們?cè)谑褂脮r(shí),常常會(huì)使用一個(gè)變量將這個(gè)唯一ID保存起來(lái),用以傳入clearTimeout,清除定時(shí)器。
接下來(lái),我們還需要考慮另外一個(gè)重要的問(wèn)題,那就是setTimeout中定義的操作,在什么時(shí)候執(zhí)行?為了引起大家的重視,我們來(lái)看看下面的例子。
var timer = setTimeout(function() {console.log('setTimeout actions.');}, 0);console.log('other actions.');
思考一下,當(dāng)我將setTimeout的延遲時(shí)間設(shè)置為0時(shí),上面的執(zhí)行順序會(huì)是什么?
在瀏覽器中的console中運(yùn)行試試看,很容易就能夠知道答案,如果你沒(méi)有猜中答案,那么我這篇文章就值得你點(diǎn)一個(gè)贊了,因?yàn)榻酉聛?lái)我分享的小知識(shí),可能會(huì)在筆試中救你一命。
在對(duì)于執(zhí)行上下文[2]的介紹中,我與大家分享了函數(shù)調(diào)用棧這種特殊數(shù)據(jù)結(jié)構(gòu)的調(diào)用特性。在這里,將會(huì)介紹另外一個(gè)特殊的隊(duì)列結(jié)構(gòu),頁(yè)面中所有由setTimeout定義的操作,都將放在同一個(gè)隊(duì)列中依次執(zhí)行。
我用下圖跟大家展示一下隊(duì)列數(shù)據(jù)結(jié)構(gòu)的特點(diǎn)。

隊(duì)列:先進(jìn)先出
而這個(gè)隊(duì)列執(zhí)行的時(shí)間,需要等待到函數(shù)調(diào)用棧清空之后才開(kāi)始執(zhí)行。即所有可執(zhí)行代碼執(zhí)行完畢之后,才會(huì)開(kāi)始執(zhí)行由setTimeout定義的操作。而這些操作進(jìn)入隊(duì)列的順序,則由設(shè)定的延遲時(shí)間來(lái)決定。
更加詳細(xì)的執(zhí)行順序,將會(huì)在事件循環(huán)的文中中描述
因此在上面這個(gè)例子中,即使我們將延遲時(shí)間設(shè)置為0,它定義的操作仍然需要等待所有代碼執(zhí)行完畢之后才開(kāi)始執(zhí)行。這里的延遲時(shí)間,并非相對(duì)于setTimeout執(zhí)行這一刻,而是相對(duì)于其他代碼執(zhí)行完畢這一刻。所以上面的例子執(zhí)行結(jié)果就非常容易理解了。
為了幫助大家理解,再來(lái)一個(gè)結(jié)合變量提升的更加復(fù)雜的例子。如果你能夠正確看出執(zhí)行順序,那么你對(duì)于函數(shù)的執(zhí)行就有了比較正確的認(rèn)識(shí)了,如果還不能,就回過(guò)頭去看看其他幾篇文章。
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();
執(zhí)行結(jié)果如圖所示。
OK,關(guān)于setTimeout就暫時(shí)先介紹到這里,我們回過(guò)頭來(lái)看看那個(gè)循環(huán)閉包的思考題。
for (var i = 1; i <= 5; i++) {setTimeout(function timer() {console.log(i);}, i * 1000);}
如果我們直接這樣寫(xiě),根據(jù)setTimeout定義的操作在函數(shù)調(diào)用棧清空之后才會(huì)執(zhí)行的特點(diǎn),for循環(huán)里定義了5個(gè)setTimeout操作。而當(dāng)這些操作開(kāi)始執(zhí)行時(shí),for循環(huán)的i值,已經(jīng)先一步變成了6。因此輸出結(jié)果總為6。而我們想要讓輸出結(jié)果依次執(zhí)行,我們就必須借助閉包的特性,每次循環(huán)時(shí),將i值保存在一個(gè)閉包中,當(dāng)setTimeout中定義的操作執(zhí)行時(shí),則訪問(wèn)對(duì)應(yīng)閉包保存的i值即可。
而我們知道在函數(shù)中閉包判定的準(zhǔn)則,即執(zhí)行時(shí)是否在內(nèi)部定義的函數(shù)中訪問(wèn)了上層作用域的變量。我們需要包裹一層自執(zhí)行函數(shù)為閉包的形成提供條件。
因此,我們只需要2個(gè)操作就可以完成題目需求,一是使用自執(zhí)行函數(shù)提供閉包條件,二是傳入i值并保存在閉包中。
for (var i = 1; i <= 5; i++) {(function (i) {setTimeout(function timer() {console.log(i);}, i * 1000);})(i)}
利用斷點(diǎn)調(diào)試,在chrome中查看執(zhí)行順序與每一個(gè)閉包中不同的i值
當(dāng)然,也可以在setTimeout的第一個(gè)參數(shù)處利用閉包。
for (var i = 1; i <= 5; i++) {setTimeout((function (i) {return function () {console.log(i);}})(i), i * 1000);}
References
[1]?詳細(xì)圖解作用域鏈與閉包:?http://www.jianshu.com/p/21a16d44f150[2]?執(zhí)行上下文:?http://www.jianshu.com/p/a6d37c77e8db
