js基礎梳理-究竟什麼是變數物件,什麼是活動物件?

煉心發表於2018-12-02

首先,回顧下上篇博文中js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行程式碼)?的執行上下文的生命週期:

3.執行上下文的生命週期

3.1 建立階段

  • 生成變數物件(Variable object, VO)
  • 建立作用域鏈(Scope chain)
  • 確定this指向

3.2 執行階段

  • 變數賦值
  • 函式引用
  • 執行其他程式碼

1.什麼是變數物件(Variable Object)

在寫程式的時候會定義很多變數和函式,那js解析器是如何找到這些變數和函式的?

變數物件是與執行上下文對應的概念,在執行上下文的建立階段,它依次儲存著在上下文中定義的以下內容:

1.1 函式的所有形參(如果是函式上下文中):

建立arguments物件。檢查當前上下文中的引數,建立該物件下的屬性與屬性值。沒有實參的話,屬性值為undefined。

1.2. 所有函式宣告:(FunctionDeclaration, FD)

檢查當前上下文的函式宣告,也就是使用function關鍵字宣告的函式。在變數物件中以函式名建立一個屬性,屬性值為指向該函式所在記憶體地址的引用。如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性。

1.3. 所有變數宣告:(var, VariableDeclaration)

檢查當前上下文中的變數宣告,每找到一個變數宣告,就在變數物件中以變數名建立一個屬性,屬性值為undefined。如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性。

2.什麼是活動物件?(activation object, AO)

  • 只有全域性上下文的變數物件允許通過VO的屬性名稱來間接訪問,在其他上下文(後面乾脆直接講函式上下文吧,我們並沒有分析eval上下文)中是不能直接訪問VO物件的。

  • 在函式上下文中,VO是不能直接訪問的,此時由活動物件AO繼續扮演VO的角色。

未進入執行階段前,變數物件中的屬性都不能訪問!但是進入到執行階段之後,變數物件轉變成了活動物件,裡面的屬性都能被訪問了,然後開始進行執行階段的操作。

因此,對於函式上下文來講,活動物件與變數物件其實都是同一個物件,只是處於執行上下文的不同生命週期。不過只有處於執行上下文棧棧頂的函式執行上下文中的變數物件,才會變成活動物件。

3.舉個例子

說了一堆概念,有點懵,對嗎?請看這個例子:

var a = 10;
function b () {
    console.log(`全域性的b函式`)
};
function bar(a, b) {
    console.log(`1`, a, b) 
    var a = 1
    function b() {
        console.log(`bar下的b函式`)
    }
    console.log(`2`, a, b) 
}
bar(2, 3)
console.log(`3`, a, b)

要想知道為什麼會這樣列印,首先,從執行上下文的建立階段來分析變數物件:

// 建立階段:
// 第一步,遇到了全域性程式碼,進入全域性上下文,此時的執行上下文棧是這樣
ECStack = [
    globalContext: {
        VO: {
            // 根據1.2,會優先處理全域性下的b函式宣告,值為該函式所在記憶體地址的引用
            b: <reference to function>,
            // 緊接著,按順序再處理bar函式宣告,此時根據1.1,因為是在全域性上下文中,並不會分析bar函式的引數
            bar: <refernce to function>,
            // 根據1.3,再處理變數,並賦值為undefined
            a: undefined
        }
    }
];
// 第二步,發現bar函式被呼叫,就又建立了一個函式上下文,此時的執行上下文棧是這樣
ECStack = [
    globalContext: {
        VO: {
            b: <reference to function b() {}>, 
            bar: <refernce to function bar() {}>,
            a: undefined
        }
    },
    <bar>functionContext: {
        VO: {
            // 根據1.1,優先分析函式的形參
            arguments: {
                0: 2,
                1: 3,
                length: 2,
                callee: bar
            },
            a: 2,
            // b: 3,
            // 根據1.2, 再分析bar函式中的函式宣告b,並且賦值為b函式所在記憶體地址的引用, 它發現VO中已經有b:3了,就會覆蓋掉它。因此上面一行中的b:3實際上不存在了。
            b: <refernce to function b() {}>
            // 根據1.3,接著分析bar函式中的變數宣告a,並且賦值為undefined, 但是發現VO中已經有a:2了,因此下面一行中的a:undefined也是會不存在的。
            // a: undefined
        }
    }
]

以上就是執行上下文中的程式碼分析階段,也就是執行上下文的建立階段。再看看執行上下文的程式碼執行階又發生了什麼。

// 執行階段:
// 第三步:首先,執行了bar(2, 3)函式,緊接著,在bar函式裡執行了console.log(`1`, a, b)。全域性上下文中依然還是VO,但是函式上下文中VO就變成了AO。並且程式碼執行到這,就已經修改了全域性上下文中的變數a.
ECStack = [
    globalContext: {
        VO: {
            b: <reference to function b() {}>, 
            bar: <refernce to function bar() {}>,
            a: 10,
        }
    },
    <bar>functionContext: {
        AO: {
            arguments: {
                0: 2,
                1: 3,
                length: 2,
                callee: bar
            },
            a: 2,
            b: <refernce to function b() {}>
        }
    }
]

// 因此會輸出結果: `1`, 2, function b() {console.log(`bar下的b函式`)};

// 第四步:執行console.log(`2`, a, b)的時候, 發現裡面的變數a被重新賦值為1了。
ECStack = [
    globalContext: {
        VO: {
            b: <reference to function b() {}>, 
            bar: <refernce to function bar() {}>,
            a: 10,
        }
    },
    <bar>functionContext: {
        AO: {
            arguments: {
                0: 2,
                1: 3,
                length: 2,
                callee: bar
            },
            a: 1,
            b: <refernce to function b() {}>
        }
    }
]
// 因此會輸出結果: `2`, 1, function b() {console.log(`bar下的b函式`)};

// 第五步,執行到console.log(`3`, a, b)的時候,ECStack發現bar函式已經執行完了,就把bar從ECStack給彈出去了。此時的執行上下文棧是這樣的。

ECStack = [
    globalContext: {
        VO: {
            b: <reference to function b() {}>, 
            bar: <refernce to function bar() {}>,
            a: 10,
        }
    }
]

// 因此會輸出結果: `3`, 10, function b() {console.log(`全域性的b函式`)}

總結一下,變數物件會有以下四種特性:

  1. 全域性上下文的變數物件初始化是全域性物件(其實這篇文章並沒有介紹這個特性,不過它也很簡單就這麼一句話而已)
  2. 函式上下文的變數物件初始化只包括Arguments物件
  3. 在進入執行上下文的時候會給變數物件新增形參,函式宣告,變數宣告等初始的屬性值
  4. 在程式碼執行階段,會再次修改變數物件的屬性值。

理解了這些,是不是發現再有一些函式提升,變數提升什麼的是不是都很簡單了。例如,你可以思考下這三段程式碼分別發生了什麼。

foo() 
var foo = function() {console.log(1)}
function foo() {console.log(2)}
foo() 
function foo() {console.log(2)}
var foo = function() {console.log(1)}
var foo = function() {console.log(1)}
function foo() {console.log(2)}
foo() 

相關文章