詳解setTimeout(fn,0)

最近面試遇到一個題目,是有關setTimeout(fn,0)和閉包應用的,題目如下:

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 0);
    console.log(i);
}

我當時想都沒想,直接給面試官說,輸出0,1,2.結果可想而知_。

很多公司面試都愛出這道題,此題考察的知識點還是蠻多的。
為了防止初學者栽在此問題上,此文稍微分析一下。
都考察了那些知識點呢?
異步、作用域、閉包,你沒聽錯,是閉包。
我們來簡化此題:

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

先打印2,后打印1。
因為是setTimeout是異步的。
正確的理解setTimeout的方式(注冊事件):
有兩個參數(shù),第一個參數(shù)是函數(shù),第二參數(shù)是時間值。
調(diào)用setTimeout時,把函數(shù)參數(shù),放到事件隊列中。等主程序運行完,再調(diào)用。
沒啥不好理解的。就像我們給按鈕綁定事件一樣:

btn.onclick = function() {
        alert(1);
};

這么寫完,會彈出1嗎。不會??!只是綁定事件而已!
必須等我們?nèi)ビ|發(fā)事件,比如去點擊這個按鈕,才會彈出1。
setTimeout也是這樣的!只是綁定事件,等主程序運行完畢后,再去調(diào)用。
setTimeout的時間值是怎么回事呢?
比如:

setTimeout(fn, 2000)

我們可以理解為2000之后,再放入事件隊列中,如果此時隊列為空,那么就直接調(diào)用fn。如果前面還有其他的事件,那就等待。
因此setTimeout是一個約會從來都不準時的童鞋。
繼續(xù)看:

setTimeout(function() {
        console.log(i);
}, 0);
var i = 1;

程序會不會報錯?
不會!而且還會準確得打印1。
為什么?
因為真正去執(zhí)行console.log(i)這句代碼時,var i = 1已經(jīng)執(zhí)行完畢了!
所以我們進行dom操作。可以先綁定事件,然后再去寫其他邏輯。

window.onload = function() {
        fn();
}
var fn = function() {
        alert('hello')
};

這么寫,完全是可以的。因為異步!

es5中是沒有塊級作用域的

for (var i = 0; i < 3; i++) {}
console.log(i);

也就說i可以在for循環(huán)體外訪問到。所以是沒有塊級作用域。
但此問題在es6里終結了,因為es6,發(fā)明了let。

這回我們再來看看原題。
原題使用了for循環(huán)。循環(huán)的本質(zhì)是干嘛的?
是為了方便我們程序員,少寫重復代碼。
讓我們倒退50年,原題等價于:

var i = 0;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
console.log(i);
i++;

因為setTimeout是注冊事件。根據(jù)前面的討論,可以都放在后面。
原題又等價于如下的寫法:

var i = 0;
console.log(i);
i++;
console.log(i);
i++;
console.log(i);
i++;
setTimeout(function() {
    console.log(i);
}, 0);
setTimeout(function() {
    console.log(i);
}, 0);
setTimeout(function() {
    console.log(i);
}, 0);

這回你明白了為啥結果是0 1 2 3 3 3了吧。

那個,說它是閉包,又是怎么回事?
為了很好的說明白這個事情,我們把它放到一個函數(shù)中:

var fn = function() {
        for (var i = 0; i < 3; i++) {
                setTimeout(function() {
                        console.log(i);
                }, 0);
                console.log(i);
        }
};
fn();

上面的函數(shù)跟我們常見另一個例子(div綁定事件)有什么區(qū)別:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = function() {
                        alert(i);
                };
        }
};
fn();

點擊每個div都會彈出3。道理是一樣的。因為alert(i)中的i是fn作用越中的,因而這是閉包。
《javascript忍者秘籍》書里把一個函數(shù)能調(diào)用全局變量,也稱閉包。
因為作者認為全局環(huán)境也可以想象成一個大的頂級函數(shù)。
怎么保證能彈出0,1, 2呢。
解決之道:以毒攻毒!
再創(chuàng)建個閉包!!

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = (function(i) {
                        return function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();

或者如下的寫法:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                (function(i) {
                        divs[i].onclick = function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();

因此原題如果也想setTimeout也彈出0,1,2的話,改成如下:

for (var i = 0; i < 3; i++) {
    setTimeout((function(i) {
        return function() {
            console.log(i);
        };
    })(i), 0);
    console.log(i);
}

總結如下:

歸根結底是要理解setTimeout(fn,0)的意思,是從下一個Event loop開始執(zhí)行,即是等當前所有腳本執(zhí)行完再運行,就是"盡可能早",還有就是閉包的運用。
如果想深入理解的話,推薦篇文章,看完你就可以完全掌握這方面的知識點了:

  1. setTimeout(fn,0)

原文地址:極客教程 https://www.geekjc.com/post/58ca7d8d32129645947aec84

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容