之前有看過一個題目,個人認為這題目非常有助於釐清觀念,所以特地另開章節來討論。
請設計一個程式,每隔一秒鐘,依序輸出12345。
恩,很簡單啊,不就是這樣囉:
for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
真的是這樣寫的嗎?請各位自己看看輸出結果吧。
想必觀念未釐清的新手,一定滿頭問號,為什會是這結果?一秒鐘後,一次出現5個6。
先解釋第一個問題,為何一秒鐘就全部輸出?
我們先拆成兩個部分來看,for迴圈跟setTimeout( )。
如果改成這樣呢?
for (var i = 1; i <= 5; i++) {
console.log(i * i);
}
請想一下這段程式在哪完成的?是不是call stack,所以不會丟到queue等待處理是吧,因此結果瞬間出現。
setTimeout( )是以非同步的方式處理callback function。不管怎樣,一定會設定時間,再丟入queue等待,說穿了,setTimeout( )存活時間極短,它的任務只是把callback function丟到他處,繼續計時。
所以for是這樣處理setTimeout( )的:
它一口氣跑完5個setTimeout( ),至於定時多少,或是callback function內容是啥,不甘它的事,它的任務只是把迴圈跑完而已。
以下是另一種表示法:
setTimeout(() => {
console.log(i);
}, 1000);
setTimeout(() => {
console.log(i);
}, 1000);
setTimeout(() => {
console.log(i);
}, 1000);
setTimeout(() => {
console.log(i);
}, 1000);
setTimeout(() => {
console.log(i);
}, 1000);
幾乎在同一時間觸發5個setTimeout( ),所以5個值才會在時間(1秒)一到,幾乎同時出現。
接下來,為何都是6呢?
之前講過,JavaScript區域跟全域變數是以函式來區分的(以var宣告的話),函式內是區域,外是全域。
所以callback function的i是全域變數,至於callback function怎麼處理i,不甘for的事,for在一瞬間就跑完迴圈,觸發5個setTimeout( ),也表示i已經累加到6了。
那這5個callback function從queue拉到call stack運算時,此時遇到的i是多少?
是6,所以才會出現5個6。
既然知道問題了,我們要怎麼解決?
先從i著手,我們要想辦法,在每跑完一次迴圈,留住i的狀態,意味著不要讓它受到全域的影響,想到了嗎?
沒錯,使用IIFE可以做到。
for (var i = 1; i <= 5; i++) {
((i) => {
setTimeout(() => {
console.log(i);
}, 1000);
})(i);
}
這時,for執行IIFE,再由IIFE觸發setTimeout( )的時候,把當時i的狀態,一併丟給callback function,所以等到callback function執行時,它所得到的i,不是全域i,而是IIFE丟給它的區域i。
那時間問題呢?
很簡單,既然能取得i的狀態,直接加入計時不就好了。
for (var i = 1; i <= 5; i++) {
((i) => {
setTimeout(() => {
console.log(i);
}, 1000 * i);
})(i);
}
解決了,不過有個更簡單的辦法,使用let宣告,將變數限制在for迴圈內。
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
參考來源:
重新認識 JavaScript: Day 18 Callback Function 與 IIFE