JavaScript深入之變數物件

冴羽發表於2017-04-11

JavaScript深入系列第四篇,具體講解執行上下文中的變數物件與活動物件。全域性上下文下的變數物件是什麼?函式上下文下的活動物件是如何分析和執行的?還有兩個思考題幫你加深印象,快來看看吧!

前言

在上篇《JavaScript深入之執行上下文棧》中講到,當 JavaScript 程式碼執行一段可執行程式碼(executable code)時,會建立對應的執行上下文(execution context)。

對於每個執行上下文,都有三個重要屬性:

  • 變數物件(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

今天重點講講建立變數物件的過程。

變數物件

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

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

全域性上下文

我們先了解一個概念,叫全域性物件。在 W3C school 中也有介紹:

全域性物件是預定義的物件,作為 JavaScript 的全域性函式和全域性屬性的佔位符。通過使用全域性物件,可以訪問所有其他所有預定義的物件、函式和屬性。

在頂層 JavaScript 程式碼中,可以用關鍵字 this 引用全域性物件。因為全域性物件是作用域鏈的頭,這意味著所有非限定性的變數和函式名都會作為該物件的屬性來查詢。

例如,當JavaScript 程式碼引用 parseInt() 函式時,它引用的是全域性物件的 parseInt 屬性。全域性物件是作用域鏈的頭,還意味著在頂層 JavaScript 程式碼中宣告的所有變數都將成為全域性物件的屬性。

如果看的不是很懂的話,容我再來介紹下全域性物件:

1.可以通過 this 引用,在客戶端 JavaScript 中,全域性物件就是 Window 物件。

console.log(this);複製程式碼

2.全域性物件是由 Object 建構函式例項化的一個物件。

console.log(this instanceof Object);複製程式碼

3.預定義了一堆,嗯,一大堆函式和屬性。

// 都能生效
console.log(Math.random());
console.log(this.Math.random());複製程式碼

4.作為全域性變數的宿主。

var a = 1;
console.log(this.a);複製程式碼

5.客戶端 JavaScript 中,全域性物件有 window 屬性指向自身。

var a = 1;
console.log(window.a);

this.window.b = 2;
console.log(this.b);複製程式碼

花了一個大篇幅介紹全域性物件,其實就想說:

全域性上下文中的變數物件就是全域性物件吶!

函式上下文

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

活動物件和變數物件其實是一個東西,只是變數物件是規範上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變數物件才會被啟用,所以才叫 activation object 吶,而只有被啟用的變數物件,也就是活動物件上的各種屬性才能被訪問。

活動物件是在進入函式上下文時刻被建立的,它通過函式的 arguments 屬性初始化。arguments 屬性值是 Arguments 物件。

執行過程

執行上下文的程式碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:

  1. 進入執行上下文
  2. 程式碼執行

進入執行上下文

當進入執行上下文時,這時候還沒有執行程式碼,

變數物件會包括:

  1. 函式的所有形參 (如果是函式上下文)

    • 由名稱和對應值組成的一個變數物件的屬性被建立
    • 沒有實參,屬性值設為 undefined
  2. 函式宣告

    • 由名稱和對應值(函式物件(function-object))組成一個變數物件的屬性被建立
    • 如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性
  3. 變數宣告

    • 由名稱和對應值(undefined)組成一個變數物件的屬性被建立;
    • 如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性

舉個例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);複製程式碼

在進入執行上下文後,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}複製程式碼

程式碼執行

在程式碼執行階段,會順序執行程式碼,根據程式碼,修改變數物件的值

還是上面的例子,當程式碼執行完後,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}複製程式碼

到這裡變數物件的建立過程就介紹完了,讓我們簡潔的總結我們上述所說:

  1. 全域性上下文的變數物件初始化是全域性物件

  2. 函式上下文的變數物件初始化只包括 Arguments 物件

  3. 在進入執行上下文時會給變數物件新增形參、函式宣告、變數宣告等初始的屬性值

  4. 在程式碼執行階段,會再次修改變數物件的屬性值

思考題

最後讓我們看幾個例子:

1.第一題

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

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???複製程式碼

第一段會報錯:Uncaught ReferenceError: a is not defined

第二段會列印:1

這是因為函式中的 "a" 並沒有通過 var 關鍵字宣告,所有不會被存放在 AO 中。

第一段執行 console 的時候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}複製程式碼

沒有 a 的值,然後就會到全域性去找,全域性也沒有,所以會報錯。

當第二段執行 console 的時候,全域性物件已經被賦予了 a 屬性,這時候就可以從全域性找到 a 的值,所以會列印 1。

2.第二題

console.log(foo);

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

var foo = 1;複製程式碼

會列印函式,而不是 undefined 。

這是因為在進入執行上下文時,首先會處理函式宣告,其次會處理變數宣告,如果如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性。

下一篇文章

《JavaScript深入之作用域鏈》

本文相關連結

《JavaScript深入之執行上下文棧》

深入系列

JavaScript深入系列目錄地址:github.com/mqyqingfeng…

JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變數物件、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

相關文章