1. 程式人生 > >JavaScript同步、非同步、回撥執行順序之經典閉包(setTimeout面試題分析)

JavaScript同步、非同步、回撥執行順序之經典閉包(setTimeout面試題分析)

同步、非同步回撥?傻傻分不清楚。

大家注意了,教大家一道口訣:

同步優先、非同步靠邊、回撥墊底!

公式表達:同步=>非同步=>回撥

這口訣的用處是什麼呢?至少應付面試,完全夠用!

例1:(經典面試題)

for(var i=0; i<5; i++){

setTimeout(function(){

console.log('i:',i);

},1000);

}

console.log(i);

此處先看結果:

5

i:5

i:5

i:5

i:5

i:5

想必大家都遇到過這樣的題目吧,那麼為什麼會是這樣的輸出結果呢?

來,跟著我念:”同步優先,非同步靠邊,回撥墊底!“

首先:for迴圈及迴圈外部的console是同步的,所以先執行for迴圈再執行外部的console.log-->同步優先

再來看:同步程式碼應該是順序執行,為什麼先輸出的是”5“呢?

原因:for迴圈是先執行,但是setTimeout的回撥函式是不能接收到引數的(回撥墊底),等for迴圈執行完,就會執行外部的console.log了,

所以先列印的會是外部console-->5

繼續:外部console執行完之後為什麼會是輸出了5個”i:5“呢?

這裡就涉及到JavaScript的執行棧和訊息佇列的概念了,

概念的詳細解釋可以看下阮老師的JavaScript執行機制詳解:再談Event Loop-阮一峰的網路日誌,或者看併發模型與Event Loop。

我拿這個例子做一下講解,JavaScript單執行緒如何處理回撥呢?JS同步的程式碼是在堆疊中順序執行的,而setTimeout回撥會先被放到訊息佇列,

for迴圈每執行一次就會放一個setTimeout到訊息佇列排隊等候,同步程式碼執行完了,再去順序執行訊息佇列上的回撥方法。

這個例子中,也就是說先執行for迴圈,按順序放置了5個setTimeout回撥到訊息佇列,然後for迴圈結束後,再執行其他的同步程式碼也就是外部的console

,至此,堆疊中已經沒有同步的程式碼了,就去訊息佇列中訊息好,發現了5個setTimeout(也是根據之前放置的順序而執行的)

到這裡:已經知道了為什麼setTimeout是最後執行的了

那麼:為什麼是5個5呢?

JavaScript在把setTimeout放到訊息佇列的過程中,迴圈的i是不會及時儲存進去的,相當於你謝了一個非同步方法,但是ajax的結果都還沒能返回,只能等到返回之後才能傳參到非同步函式中,也就是同步程式碼都還沒執行完,i是不會被傳入到回撥函式的。

在這裡,因為i是用var定義的,所以是全域性變數(因為此處沒有其他的函式,如果有其他的函式,那i就是此函式內部變數),當for迴圈執行完畢,i值為5,從外部的console也可看出,那麼當同步函式執行完畢,回撥接收到的i也就是5了(很多人都會以為setTimeout裡面的i會是for迴圈過程中的i值,這種理解是不正確的)

例2:我們在例1中加上一行程式碼。

for(var i=0; i<5; i++){

setTimeout(function(){

console.log('2',i);

},1000);

console.log('1:',i);//新加程式碼

}

console.log(i);

老規矩,先看列印結果:

1:0

1:1

1:2

1:3

1:4

5

2:5

2:5

2:5

2:5

2:5

牢記口訣:同步=>非同步=>回撥(強化記憶)

這個例子的補充,可以很清楚的看到先執行for迴圈,迴圈裡面的console是同步的,所以先輸出,結束後再執行外部的console,最後再執行setTimeout回撥函式!

是不是so easy?

那麼面試官如果再問,如何解決這個問題?

很簡單,當然是ES6中的最新特性!let!!!

例3:

for(let i=0; i<5; i++){

setTimeout(function(){

console.log('2',i);

},1000);

}

console.log(i);

先看輸出!反向理解!

i is not defined

2:0

2:1

2:2

2:3

2:4

咦~為什麼外部的console.log(i)會報錯呢?

你這個口訣是不是哪裡不對勁呢?

let是ES6的語法(ECMAScript 6,JavaScript最新規範,主流瀏覽器已基本支援該規範,並持續向該規範靠攏,其中,IE比較特殊想必大家都知道的!

在PC端開發的時候,要注意IE9以下的相容,移動端開發時,可以比較放心了!

目前實際專案中,安全的做法是運用babel工具將ES6解釋為ES5)

ES5中的變數作用域是函式,而let語法的作用域是當前塊,這裡就是for迴圈體了。

我們來分析一下,用了let作為變數i的定義之後,作用與程式碼塊中,此處也就是指for迴圈,for迴圈每執行一次,都會先給迴圈內的setTimeout傳引數i,每次傳入的引數依次是0,1,2,3,4,每接受一次引數然後迴圈內的setTimeout被放到訊息佇列(帶入了傳入的引數i,與之前var定義的i是不同的),for迴圈執行完畢,i不在作用在當前塊之外的程式碼中,所以外部的同步程式碼console輸出的i為定義!當同步程式碼執行完畢,再依次取出訊息佇列中帶有不同引數的回撥函式,所有輸出的結果是如上所示!

在這裡let本質上就是形成了一個閉包。也就是下面例4這種寫法一樣的意思,如果面試官說用下面例4的方式,你可以正兒八經的告訴他:這就是一個意思!

這也就是為什麼有人說let是語法糖!

例:4:

var loop = function(_i){

setTimeout(function(){

console.log('2:',_i);

},1000);

}

for(var _i=0; i<5; _i++){

loop(i)

}

console.log(_i);

//輸出

5

2:0

2:1

2:2

2:3

2:4

或許這或讓面試官聯想到閉包問題,什麼是閉包呢?耐心往下看,後面講。

回到ES5,你是不是就發現適合我的口訣了?同步優先=>非同步靠邊=>回撥墊底!

而用let的時候。你看不懂?你需要真正的瞭解ES6的語法原理!

注意!

閉包概念:當內部函式以某一種方式被任何一個外部函式作用域訪問時,一個閉包就產生了!

也就是說loop(_1)是外部函式,setTimeout是內部函式,當setTimeout被loop的變數訪問的時候,就形成了一個閉包!

例5:

function test(){

var a=10;

var b = function(){

console.log(a);

}

b();

}

test();//輸出10

口訣繼續:同步優先=>非同步靠邊=>回撥墊底

先執行函式test,然後JS進入test函式內部,定義了一個變數,然後執行函式b,進入函式b內部,列印a,這裡都是同步程式碼,那麼這裡怎麼解釋閉包?

解釋:函式test是外部函式,函式b是內部函式,當函式b被外部函式test的變數訪問的時候,就形成了閉包。

迴歸正題!

上面主要講了同步、非同步、回撥的執行順序問題,接著我就舉一個簡單的同步、非同步、回撥的例子

例6:

let a=new Promise(

function(){

console.log(1);

setTimeout(()=>consoel.log(2),0);

console.log(3);

console.log(4);

resolve(true);

}

);

a.then(v=>{

console.log(8)

});

let b=new Promise(

function(){

console.log(5);

setTimeout(()=>console.log(6),0);

}

);

console.log(7);

一眼看不出名堂,不過不慌!

先讀口訣:同步優先=>非同步靠邊=>回撥墊底

1、看同步程式碼:a變數是一個Promise,我們知道Promise是非同步的,是指他的then()和catch()方法,Promise本身還是同步的,所以這裡先執行a變數內部的Promise同步程式碼(同步優先)

2、Promise內部有4個console,第二個是一個setTimeout回撥(回撥墊底)。所以這裡先輸出1,3,4,回撥的方法當然是丟到訊息佇列中排隊等著。

3、接著執行resolve(true),進入then(),then是非同步,下面還有同步程式碼沒執行,所以也被丟到訊息佇列中排隊等待(非同步靠邊)

4、b變數也是一個Promise,和a一樣,先執行內部的同步程式碼,輸出5,setTimeout滾如訊息佇列排隊等待。

5、最下面同步輸出7

6、同步程式碼執行完了,Javascript就跑到訊息佇列呼叫非同步的程式碼:這裡的非同步只有then,輸出8

7、非同步也執行完了,接著就是去訊息佇列中依次找到回調了:這裡2個回撥在排隊,setTimeout時間引數都是0,不做任何影響,只是跟他們在訊息佇列中的排隊順序有關,所以先輸出a裡面的2,最後輸出b裡面的回撥6

8、最終輸出結果是:1、3、4、5、7、8、2、6

PS:如果想變得有趣一點的話,我們可以稍微做一點點修改,把a裡面Promise的setTimeout的時間蠶食0改成2,也就是2ms後執行,為什麼不是1ms,1ms的話,瀏覽器都還沒有反應過來呢。改成>=2的數字才能看到2個setTimeout的輸出順序發生了變化。所以回撥函式正常情況下是在訊息佇列中順序執行的,但是使用setTimeout的時候,還需要注意時間大小也會改變它的順序(感覺上是改變了順序,其實先讀檢索的訊息佇列上的回撥還是a,不過因為時間引數的原因,被滯後2ms執行了)。

口訣不一定是萬能的,不過作為一種輔助,更重要的還是要理解JavaScript的執行機制,才能對程式碼的執行順序有清晰的路線。

還有async/await等其他非同步的方案,不管是那種非同步,基本都試用於這個口訣,對於新手來說,可以快速讀懂面試官出的js筆試題目,做出快速準確並且也是面試官希望得到的答案,以後再也不用怕做到類似的筆試題目啦!

PS:特殊情況下不適應口訣也很正常!JavaScript博大精深,不是一句話就能概括出來的,隨著ES6的推廣與開拓,JavaScript一定會有更為快速的發展!但是萬變不離其宗,掌握JavaScript的底層機制始終異常重要!

最後:再念一次口訣!同步優先=>非同步靠邊=>回撥墊底!

(原文自前端教程=>hyy1115)