在一些文章中或者工作面試問題上,會遇見這種看似簡單的經(jīng)典問題。
公司主營業(yè)務(wù):成都網(wǎng)站建設(shè)、成都網(wǎng)站制作、移動網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。成都創(chuàng)新互聯(lián)是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊。公司秉承以“開放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團(tuán)隊有機(jī)會用頭腦與智慧不斷的給客戶帶來驚喜。成都創(chuàng)新互聯(lián)推出莘縣免費做網(wǎng)站回饋大家。
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
/*output
'hello word'
5
5
5
5
5
*/
對于老鳥來說這種問題不足掛齒,但是如果你是新手正在學(xué)習(xí) js 的路上如火如荼或是剛好遇到了此類問題一知半解,那么這篇文章將給你帶來原理和解答。 小小問題背后別有洞天。
JS 是典型的單線程語言,所謂單線程就是只能同時執(zhí)行一個任務(wù)。
之所以是單線程而不是多線程,是為了避免多線程對同一 DOM 對象操作的沖突。比如 A 線程創(chuàng)造一
操作系統(tǒng)的進(jìn)程和線程:
對于操作系統(tǒng)來說,一個任務(wù)就是一個進(jìn)程(Process),比如打開一個瀏覽器就是啟動一個瀏覽器進(jìn)程,打開一個記事本就啟動了一個記事本進(jìn)程,打開兩個記事本就啟動了兩個記事本進(jìn)程,打開一個Word就啟動了一個Word進(jìn)程。
有些進(jìn)程還不止同時干一件事,比如Word,它可以同時進(jìn)行打字、拼寫檢查、打印等事情。在一個進(jìn)程內(nèi)部,要同時干多件事,就需要同時運行多個“子任務(wù)”,我們把進(jìn)程內(nèi)的這些“子任務(wù)”稱為線程(Thread)。
一個進(jìn)程至少有一個線程,復(fù)雜的進(jìn)程有多個線程。操作系統(tǒng)通過多核cpu快速交替執(zhí)行這些線程就給人一種同時執(zhí)行的感覺。
單線程就意味著,所有任務(wù)需要排隊,前一個任務(wù)結(jié)束,后一個任務(wù)才會執(zhí)行。前面的任務(wù)耗時過長,后面的任務(wù)也得硬著頭皮等待。而任務(wù)執(zhí)行慢通常不是 CPU 性能不行,而是 I/O 設(shè)備操作耗時長,比如Ajax操作從網(wǎng)絡(luò)獲取數(shù)據(jù)。
JS 設(shè)計者意識到,遇到這種情況主線程可以完全不管 I/O 設(shè)備的結(jié)果,先掛起 I/O 耗時的任務(wù),然后執(zhí)行排在后面的任務(wù)。直到 I/O 設(shè)備返回了結(jié)果,并發(fā)來了通知,再回過來執(zhí)行先前掛起的任務(wù)。
所以,設(shè)計者把瀏覽器的程序任務(wù)可以分為兩種,同步任務(wù)和異步任務(wù):
例子中的代碼運行機(jī)制看這里:
一文說清 JS 運行時環(huán)境(Event Loop)
回過頭來看文章開頭那段代碼
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
setTimeout()
方法中作為 Timers 屬于異步任務(wù),每次循環(huán)就會被分發(fā)到 WEB API's 的容器中,并且作為參數(shù)的匿名函數(shù)也會被存儲到內(nèi)存堆中,也就是說這種操作 JS 運行時 會重復(fù) 5 次。console.log('hello word')
在循環(huán)完成后被推入執(zhí)行棧執(zhí)行,打印字符串。i
,重復(fù) 5 次結(jié)束。所以實質(zhì)上可以看作(取巧方便理解,非實質(zhì)):
// 同步執(zhí)行
var i;
for(i = 0; i < 5; i++) {
}
// 同步執(zhí)行
console.log('hello word');
// 異步執(zhí)行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
作用域簡單的說就是 JS 函數(shù)當(dāng)前執(zhí)行的上下文語境。函數(shù)在這個上下文語境中才能訪問和引用這個語境中的其他變量。子作用域可以訪問和引用父作用域中的變量,反之不行。
一個函數(shù)對象在JS中被創(chuàng)建的時候同時創(chuàng)建了閉包,閉包是由該函數(shù)對象和它所在的語境而構(gòu)成的一個組合。通常返回一個函數(shù)的引用。
// 一個典型的閉包
function makeFunc() {
var text = "hello world";
function displayName() {
console.log(text);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();// hello world
我們可以利用閉包的原理讓定時器打印出 0, 1, 2, 3, 4。
for(var i = 0; i < 5; i++) {
((i) => {
setTimeout(function () {
console.log(i);
});
})(i);
}
console.log('hello word');
在上面的代碼中,使用了一個技巧 立即函數(shù) 給計時器單獨提供了一個新的作用域(上下文語境),加上里面的計時器就剛好組成了一個異步的閉包組合,而且是立刻調(diào)用的。
通過上面的手段就可以很好的避免var
聲明的循環(huán)變量暴露在全局作用域帶來的問題。從而打印出 0, 1, 2, 3, 4。
通過let
聲明循環(huán)變量也是很好的解決手段,let
允許你聲明一個被限制在塊作用域中的變量,這個就是塊級作用域。
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
console.log('hello word');
let
是ES6語法,而塊級作用域的出現(xiàn)解決了var
循環(huán)變量泄露為全局變量的問題和變量覆蓋的問題。
對于不能兼容ES6的瀏覽器,我們也可以使用ES5try...catch
語句,形成類似閉包的效果。
for(var i = 0; i < 5; i++) {
try {
throw(i)
} catch(j) {
setTimeout(function () {
console.log(j);
});
}
}
console.log('hello word');
回到上面的代碼,著重說下let
是如何做到每次循環(huán)變量i
能夠保存當(dāng)前的上下文語境,并傳值傳給下次循環(huán)的:
let
關(guān)鍵字聲明的變量i
至始至終都是屬于for
循環(huán)塊級作用域內(nèi)的局部變量。for
循環(huán)每迭代一次,局部變量i
就將當(dāng)前的狀態(tài)單獨保存在內(nèi)存中。for
循環(huán)的塊級作用域?qū)?yīng)的變量對象 => 全局變量對象,所以匿名函數(shù)和for
循環(huán)的塊級作用域(上下文語境)形成了閉包這樣的關(guān)系。i
值都是局部變量i
單獨保存在內(nèi)存中值。