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

同步、異步回調?傻傻分不清楚。前端

你們注意了,教你們一道口訣:面試

同步優先、異步靠邊、回調墊底!ajax

公式表達:同步=>異步=>回調瀏覽器

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

例1:(經典面試題)babel

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

  setTimeout(function(){併發

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

  },1000);async

}

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);

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

先讀口訣:同步優先=>異步靠邊=>回調墊底

一、看同步代碼:a變量是一個Promise,咱們知道Promise是異步的,是指他的then()和catch()方法,Promise自己仍是同步的,因此這裏先執行a變量內部的Promise同步代碼(同步優先)

二、Promise內部有4個console,第二個是一個setTimeout回調(回調墊底)。因此這裏先輸出1,3,4,回調的方法固然是丟到消息隊列中排隊等着。

三、接着執行resolve(true),進入then(),then是異步,下面還有同步代碼沒執行,因此也被丟到消息隊列中排隊等待(異步靠邊)

四、b變量也是一個Promise,和a同樣,先執行內部的同步代碼,輸出5,setTimeout滾如消息隊列排隊等待。

五、最下面同步輸出7

六、同步代碼執行完了,Javascript就跑到消息隊列呼叫異步的代碼:這裏的異步只有then,輸出8

七、異步也執行完了,接着就是去消息隊列中依次找到回調了:這裏2個回調在排隊,setTimeout時間參數都是0,不作任何影響,只是跟他們在消息隊列中的排隊順序有關,因此先輸出a裏面的2,最後輸出b裏面的回調6

八、最終輸出結果是:一、三、四、五、七、八、二、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)

相關文章
相關標籤/搜索