js 執行上下文

景在峰中發表於2018-10-12

執行上下文(Execution Context)

每次當控制器轉到可執行程式碼的時候,就會進入一個執行上下文。執行上下文可以理解為當前程式碼的執行環境。

那什麼是可執行程式碼

  • 全域性程式碼(全域性環境):你的程式碼首次執行的預設環境,例如載入外部的js檔案或者本地標籤內的程式碼。全域性程式碼不包括任何function體內的程式碼。 這個是預設的程式碼執行環境,一旦程式碼被載入,引擎最先進入的就是這個環境。
  • 函式程式碼(函式環境):當函式被呼叫執行時,會進入當前函式中執行環境
  • eval(不建議使用,可忽略)

執行上下文棧

我們都知道,瀏覽器中的JS直譯器被實現為單執行緒,這實際上意味著,在瀏覽器中一次只會發生一件事,而在一個JavaScript程式中,必定會產生多個執行上下文,JavaScript引擎會以棧的方式來處理它們,這個棧,我們稱其為執行棧(呼叫棧)。棧底永遠都是全域性上下文,而棧頂就是當前正在執行的上下文,下面的這個圖是單執行緒的棧的一個抽象的表示:

image

當瀏覽器首次載入你的指令碼,它將預設進入全域性執行上下文。如果,你在你的全域性程式碼中呼叫一個函式,你程式的時序將進入被呼叫的函式,並建立一個新的執行上下文,並將新建立的上下文壓入執行棧的頂部。

如果你呼叫當前函式內部的其他函式,相同的事情會在此上演。程式碼的執行流程進入內部函式,建立一個新的執行上下文並把它壓入執行棧的頂部。瀏覽器總會執行位於棧頂的執行上下文,一旦當前上下文函式執行結束,它將被從棧頂彈出,並將上下文控制權交給當前的棧。這樣,堆疊中的上下文就會被依次執行並且彈出堆疊,直到回到全域性的上下文。

詳細瞭解了這個過程之後,我們就可以對執行上下文總結一些結論了。

  • 單執行緒
  • 同步執行,只有棧頂的上下文處於執行中,其他上下文需要等待
  • 全域性上下文只有唯一的一個,它在瀏覽器關閉時出棧
  • 函式的執行上下文的個數沒有限制
  • 每次函式被呼叫建立新的執行上下文,包括呼叫自己。

執行上下文的細節

可以將每個執行上下文抽象為一個物件並有三個屬性:

image

executionContextObj = {
    variableObject: { /*函式 arguments/引數,內部變數和函式宣告 */ }, 
    scopeChain: { /* 變數物件(variableObject)+ 所有父執行上下文的變數物件*/ },
    this: {} 
}
複製程式碼

我們已經知道,當呼叫一個函式時(啟用),一個新的執行上下文就會被建立。而一個執行上下文的生命週期可以分為兩個階段。

  1. 建立階段【當函式被呼叫,但未執行任何其內部程式碼之前】:
    • 建立變數物件(VO)。
    • 建立作用域鏈(Scope Chain)
    • 確定this指向。
  2. 啟用/程式碼執行階段:
    • 會完成變數賦值,函式引用,以及執行其他程式碼

變數物件(VO/AO)

變數物件是與執行上下文相關的資料作用域,儲存了在上下文中定義的變數和函式宣告。

因為不同執行上下文下的變數物件稍有不同,所以我們來聊聊全域性上下文下的變數物件和函式上下文下的變數物件。

全域性上下文中的變數物件

一句話全域性上下文中的變數物件就是全域性物件,客戶端可以通過window訪問這個物件。

函式上下文中的變數物件

在函式上下文中,我們用活動物件(activation object, AO)來表示變數物件。

變數物件的建立,依次經歷了以下幾個過程(該過程是有先後順序的:函式的形參==>>函式宣告==>>變數宣告)。

  1. 建立arguments物件。檢查當前上下文中的引數,在變數物件中以形參名建立一個屬性,實參作為該屬性值。
  2. 檢查當前上下文的函式宣告,也就是使用function關鍵字宣告的函式。在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用。如果變數物件已經包含了相同名字的屬性,則替換它的值。
  3. 檢查當前上下文中的變數宣告,每找到一個變數宣告,就在變數物件中以變數名建立一個屬性,屬性值為undefined。如果變數名和已經宣告的函式名或者函式的引數名相同,則不會影響已經存在的屬性。

我們來看個列子詳細瞭解下變數物件演變的一個過程

    function test(a) {
        var b = 1;
        function c () {};
    }
    test(10);
    // 執行上下文建立階段
    testExecutionContext = {
        AO: {
            arguments: {
                length: 1,
                0: 10,
            },
            a: 10,
            c: pointer to Function c,
            b: undefined
        },   
        scopeChain: { ... },
        this: { ... }
    }
    // 執行上下文執行階段 
    testExecutionContext = {
        AO: {
            arguments: {
                length: 1,
                0: 10,
            },
            a: 10,
            c: pointer to Function c,
            b: 1
        },   
        scopeChain: { ... },
        this: { ... }
    }
複製程式碼

如何理解函式宣告過程中如果變數物件已經包含了相同名字的屬性,則替換它的值這句話? 看如下這段程式碼:

function foo1(a){
    console.log(a)
    function a(){} 
}
foo1(20)
複製程式碼

根據上面的介紹,我們知道VO建立過程中,函式形參的優先順序是高於函式的宣告的,結果是函式體內部宣告的function a(){}覆蓋了函式形參a的宣告,因此最後輸出a是一個function

如何理解變數宣告過程中如果變數名和已經宣告的函式名或者函式的引數名相同,則不會影響已經存在的屬性這句話?

//情景一:與引數名相同
function foo2(a){
    console.log(a)
    var a = 10
}
foo2(20)

//情景二:與函式名相同

function foo2(a){
    console.log(a)
    var a = 10
    function a(){}
}
foo2(20)
複製程式碼

相關文章