JavaScript的執行上下文


在JavaScript的運行過程中,經常會遇到一些"奇怪"的行為,不理解為什么JavaScript會這么工作。

這時候可能就需要了解一下JavaScript執行過程中的相關內容了。

執行上下文

在JavaScript中有三種代碼運行環境:

  • Global Code
    • JavaScript代碼開始運行的默認環境
  • Function Code
    • 代碼進入一個JavaScript函數
  • Eval Code
    • 使用eval()執行代碼

為了表示不同的運行環境,JavaScript中有一個執行上下文(Execution context,EC)的概念。也就是說,當JavaScript代碼執行的時候,會進入不同的執行上下文,這些執行上下文就構成了一個執行上下文棧(Execution context stack,ECS)

例如對如下面的JavaScript代碼:

var a = "global var";

function foo(){
    console.log(a);
}

function outerFunc(){
    var b = "var in outerFunc";
    console.log(b);
    
    function innerFunc(){
        var c = "var in innerFunc";
        console.log(c);
        foo();
    }
    
    innerFunc();
}


outerFunc()

代碼首先進入Global Execution Context,然后依次進入outerFunc,innerFunc和foo的執行上下文,執行上下文棧就可以表示為:

當JavaScript代碼執行的時候,第一個進入的總是默認的Global Execution Context,所以說它總是在ECS的最底部。

對於每個Execution Context都有三個重要的屬性,變量對象(Variable object,VO),作用域鏈(Scope chain)和this。這三個屬性跟代碼運行的行為有很重要的關系,下面會一一介紹。

當然,除了這三個屬性之外,根據實現的需要,Execution Context還可以有一些附加屬性。

VO和AO

從上面看到,在Execution Context中,會保存變量對象(Variable object,VO),下面就看看變量對象是什么。

變量對象(Variable object)

變量對象是與執行上下文相關的數據作用域。它是一個與上下文相關的特殊對象,其中存儲了在上下文中定義的變量和函數聲明。也就是說,一般VO中會包含以下信息:

  • 變量 (var, Variable Declaration);
  • 函數聲明 (Function Declaration, FD);
  • 函數的形參

當JavaScript代碼運行中,如果試圖尋找一個變量的時候,就會首先查找VO。對於前面例子中的代碼,Global Execution Context中的VO就可以表示如下:

注意,假如上面的例子代碼中有下面兩個語句,Global VO仍將不變。

(function bar(){}) // function expression, FE
baz = "property of global object"

也就是說,對於VO,是有下面兩種特殊情況的:

  • 函數表達式(與函數聲明相對)不包含在VO之中
  • 沒有使用var聲明的變量(這種變量是,"全局"的聲明方式,只是給Global添加了一個屬性,並不在VO中)

活動對象(Activation object)

只有全局上下文的變量對象允許通過VO的屬性名稱間接訪問;在函數執行上下文中,VO是不能直接訪問的,此時由激活對象(Activation Object,縮寫為AO)扮演VO的角色。激活對象 是在進入函數上下文時刻被創建的,它通過函數的arguments屬性初始化。

Arguments Objects 是函數上下文里的激活對象AO中的內部對象,它包括下列屬性:

  1. callee:指向當前函數的引用
  2. length: 真正傳遞的參數的個數
  3. properties-indexes:就是函數的參數值(按參數列表從左到右排列)

對於VO和AO的關系可以理解為,VO在不同的Execution Context中會有不同的表現:當在Global Execution Context中,可以直接使用VO;但是,在函數Execution Context中,AO就會被創建。

當上面的例子開始執行outerFunc的時候,就會有一個outerFunc的AO被創建:

通過上面的介紹,我們現在了解了VO和AO是什么,以及他們之間的關系了。下面就需要看看JavaScript解釋器是怎么執行一段代碼,以及設置VO和AO了。

細看Execution Context

當一段JavaScript代碼執行的時候,JavaScript解釋器會創建Execution Context,其實這里會有兩個階段:

  • 創建階段(當函數被調用,但是開始執行函數內部代碼之前)
    • 創建Scope chain
    • 創建VO/AO(variables, functions and arguments)
    • 設置this的值
  • 激活/代碼執行階段
    • 設置變量的值、函數的引用,然后解釋/執行代碼

這里想要詳細介紹一下"創建VO/AO"中的一些細節,因為這些內容將直接影響代碼運行的行為。

對於"創建VO/AO"這一步,JavaScript解釋器主要做了下面的事情:

  • 根據函數的參數,創建並初始化arguments object
  • 掃描函數內部代碼,查找函數聲明(Function declaration)
    • 對於所有找到的函數聲明,將函數名和函數引用存入VO/AO中
    • 如果VO/AO中已經有同名的函數,那么就進行覆蓋
  • 掃描函數內部代碼,查找變量聲明(Variable declaration)
    • 對於所有找到的變量聲明,將變量名存入VO/AO中,並初始化為"undefined"
    • 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性

看下面的例子:

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

對於上面的代碼,在"創建階段",可以得到下面的Execution Context object:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

在"激活/代碼執行階段",Execution Context object就被更新為:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

例子分析

前面介紹了Execution Context,VO/AO等這么多的理論知識,當然是為了方便我們去分析代碼中的一些行為。這里,就通過幾個簡單的例子,結合上面的概念來分析結果。

Example 1

首先看第一個例子:

(function(){
    console.log(bar);
    console.log(baz);
    
    var bar = 20;
    
    function baz(){
        console.log("baz");
    }
    
})()

在Chrome中運行代碼運行后將輸出:

代碼解釋:匿名函數會首先進入"創建結果",JavaScript解釋器會創建一個"Function Execution Context",然后創建Scope chain,VO/AO和this。根據前面的介紹,解釋器會掃描函數和變量聲明,如下的AO會被創建:

所以,對於bar,我們會得到"undefined"這個輸出,表現的行為就是,我們在聲明一個變量之前就訪問了這個變量。這個就是JavaScript中"Hoisting"。

Example 2

接着上面的例子,進行一些修改:

(function(){
    console.log(bar);
    console.log(baz);
    
    bar = 20;
    console.log(window.bar);
    console.log(bar);
    
    function baz(){
        console.log("baz");
    }
    
})()

運行這段代碼會得到"bar is not defined(…)"錯誤。當代碼執行到"console.log(bar);"的時候,會去AO中查找"bar"。但是,根據前面的解釋,函數中的"bar"並沒有通過var關鍵字聲明,所有不會被存放在AO中,也就有了這個錯誤。

注釋掉"console.log(bar);",再次運行代碼,可以得到下面結果。"bar"在"激活/代碼執行階段"被創建。

Example 3

現在來看最后一個例子:

(function(){
    console.log(foo);
    console.log(bar);
    console.log(baz);
    
    var foo = function(){};
    
    function bar(){
        console.log("bar");
    }
    
    var bar = 20;
    console.log(bar);
    
    function baz(){
        console.log("baz");
    }
    
})()

代碼的運行結果為:

代碼中,最"奇怪"的地方應該就是"bar"的輸出了,第一次是一個函數,第二次是"20"。

其實也很好解釋,回到前面對"創建VO/AO"的介紹,在創建VO/AO過程中,解釋器會先掃描函數聲明,然后"foo: <function>"就被保存在了AO中;但解釋器掃描變量聲明的時候,雖然發現"var bar = 20;",但是因為"foo"在AO中已經存在,所以就沒有任何操作了。

但是,當代碼執行到第二句"console.log(bar);"的時候,"激活/代碼執行階段"已經把AO中的"bar"重新設置了。

總結

本文介紹了JavaScript中的執行上下文(Execution Context),以及VO/AO等概念,最后通過幾個例子展示了這幾個概念對我們了解JavaScript代碼運行的重要性。

通過對VO/AO在"創建階段"的具體細節,如何掃描函數聲明和變量聲明,就可以對JavaScript中的"Hoisting"有清晰的認識。所以說,了解JavaScript解釋器的行為,以及相關的概念,對理解JavaScript代碼的行為是很有幫助的。

后面會對Execution Context中的Scope chain和this進行介紹。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM