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

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

初心-楊瑞超個人部落格誠邀您加入qq群(IT-程式猿-技術交流群):757345416

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

同步優先、非同步靠邊、回撥墊底(讀起來不順)

用公式表達就是:

同步 => 非同步 => 回撥

有一道經典的面試題:

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

分析:
1、for迴圈和迴圈體外部的console是同步的,所以先執行for迴圈,再執行外部的console.log。(同步優先)

2、for迴圈裡面有一個setTimeout回撥,他是墊底的存在,只能最後執行。(回撥墊底)

那麼,為什麼我們最先輸出的是5呢?

非常好理解,for迴圈先執行,但是不會給setTimeout傳參(回撥墊底),等for迴圈執行完,就會給setTimeout傳參,而外部的console打印出5是因為for迴圈執行完成了。

我們給第一個例子加一行程式碼。

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迴圈,for迴圈裡面的console是同步的,所以先輸出,for迴圈結束後,執行外部的console輸出5,最後再執行setTimeout回撥 55555。

那麼怎麼解決此問題呢?

我們用let解決

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

為什麼i會報錯呢?解析:

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

在這裡,let本質上就是形成了一個閉包。也就是下面這種寫法一樣的意思。下面程式碼同理:

var loop = function (_i) {
    setTimeout(function() {
        console.log('2:', _i);
    }, 1000);
};

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

console.log(i);

解析:我們來分析一下,用了let作為變數i的定義之後,for迴圈每執行一次,都會先給setTimeout傳參,準確的說是給loop傳參,loop形成了一個閉包,這樣就執行了5個loop,每個loop傳的引數分別是0,1,2,3,4,然後loop裡面的setTimeout會進入訊息佇列排隊等候。當外部的console執行完畢,因為for迴圈裡的i變成了一個新的變數 _i ,所以在外部的console.log(i)是不存在的。

現在可以解釋閉包的概念了:當內部函式以某一種方式被任何一個外部函式作用域訪問時,一個閉包就產生了。

我知道你又要我解釋這句話了,loop(_i)是外部函式,setTimeout是內部函式,當setTimeout被loop的變數訪問的時候,就形成了一個閉包。

閉包例項:

function t() {
    var a = 10;
    var b = function() {
        console.log(a);    
    }
    b();
}
t(); //輸出 10

接著我就舉一個包含同步、非同步、回撥的例子

let a = new Promise(
  function(resolve, reject) {
    console.log(1)
    setTimeout(() => console.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同步程式碼。(同步優先)。

console.log(1)
setTimeout(() => console.log(2), 0) //回撥
console.log(3)
console.log(4)

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

3、接著執行resolve(true),進入then(),then是非同步,下面還有同步沒執行完呢,所以then也滾去訊息佇列排隊等候。(真可憐)(非同步靠邊)
4、b變數也是一個Promise,和a一樣,執行內部的同步程式碼,輸出5,setTimeout滾去訊息佇列排隊等候。

5、最下面同步輸出7。

6、同步的程式碼執行完了,JavaScript就跑去訊息佇列呼叫非同步的程式碼:非同步,出來執行了。這裡只有一個非同步then,所以輸出8。

7、非同步也over,輪到回撥的孩子們:回撥,出來執行了。這裡有2個回撥在排隊,他們的時間都設定為0,所以不受時間影響,只跟排隊先後順序有關。則先輸出a裡面的回撥2,最後輸出b裡面的回撥6。

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

我們還可以稍微做一點修改,把a裡面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),對,時間改成了2ms,為什麼不改成1試試呢?1ms的話,瀏覽器都還沒有反應過來呢。你改成大於或等於2的數字就能看到2個setTimeout的輸出順序發生了變化。所以回撥函式正常情況下是在訊息佇列順序執行的,但是使用setTimeout的時候,還需要注意時間的大小也會改變它的順序。

文章到此結束,希望對你的學習有幫助!

注:本文章是對原文https://segmentfault.com/a/1190000008922457二次編輯,如有侵權,請聯絡作者刪除。