深入 JavaScript 變數物件

語言: CN / TW / HK

歡迎關注微信公眾號:前端閱讀室

前言

在上節《深入 JavaScript 執行上下文棧——Web 前端進階系列第三節》我們講到,JavaScript 引擎執行一段可執行程式碼時,會建立對應的執行上下文。

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

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

今天我們來重點講解變數物件。

變數物件

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

執行上下文分為兩種:全域性上下文和函式上下文,接下來我們來分別講解這兩種上下文的變數物件。

全域性上下文中變數物件

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

下面我們來了解一下全域性物件,在 W3school 中的介紹有:

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

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

  3. 由於全域性物件在作用域鏈最底端,這也意味著在頂層 JavaScript 程式碼中宣告的變數都將成為全域性物件的屬性。

字面上大家理解起來可能比較抽象,接下來我們結合具體例子作進一步講解。

  1. 在頂層 JavaScript 程式碼中,可以用關鍵字 this 引用全域性物件。在瀏覽器 JavaScript 中,全域性物件是 window。在 node.js 中,全域性物件是 global。

js console.log(this); // window console.log(this === window); // true

  1. 全域性物件是 JavaScript 的全域性函式和全域性屬性的佔位符。在頂層 JavaScript 程式碼中宣告的變數都將成為全域性物件的屬性。

```js // 宣告的變數成為了全域性物件的屬性 var a = 1; console.log(this.a); // 1

// 宣告的函式成為了全域性物件的屬性 function b() {} console.log(this.b); // function b ```

  1. 通過使用全域性物件,可以訪問全域性函式和全域性屬性,也可以訪問所有其他預定義的物件、函式和屬性。

js // 使用全域性物件訪問全域性屬性 Math,它是一個物件,它擁有 random 方法。 console.log(this.Math.random()); // 列印一個隨機數

  1. 所有非限定性的變數和函式名都會作為該物件的屬性來查詢。

js // 這裡的 Math 是非限定性的函式名 console.log(Math.random()); // 列印一個隨機數

  1. 全域性物件是 Object 建構函式的例項,這也意味著 Object.prototype(原型)上預定義的屬性和方法,是可以通過全域性物件訪問到的。

js console.log(this instanceof Object); // true

  1. 在瀏覽器 JavaScript 中,全域性物件有 window 屬性且指向自身。

js console.log(this.window === this); // true

函式上下文中的變數物件

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

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

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

執行過程

函式上下文的程式碼執行過程共分成兩個階段,分別是:預編譯和執行。

預編譯

  • 建立 AO 物件,尋找形參和變數宣告

  • 把形參和變數名作為 AO 物件的屬性名,值為 undefined

  • 把實參賦給形參,實參形參相統一

  • 尋找函式宣告,值為函式體

我們來看個例子:

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

b = 3; }

foo(1); ```

這個函式在預編譯完成後,AO 會變為:

js AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }

程式碼執行

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

上面的例子當代碼執行完,AO 會變為:

js 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. 第一題

來看下面兩端程式碼,分別會列印什麼?

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

foo(); ```

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

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

第二段會列印:1。

因為第一段程式碼 a 沒有變數宣告,所以函式執行上下文的 AO 中沒有 a 變數的定義,此時 AO 的值是:

AO = { arguments: { length: 0 } }

執行列印時,在函式執行上下文的 AO 中沒有找到 a 變數的定義,然後就會去全域性上下文中找,發現全域性也沒有,所以就會報未定義的錯。

第二段程式碼,沒有使用 var 關鍵字宣告的變數會成為全域性物件的屬性,所以執行列印時,會從全域性物件找到 a 的值,所以會列印 1。

  1. 第二題

```js console.log(foo);

function foo() {}

var foo = 1; ```

會列印 foo 函式,而不是 undefined。

因為在預編譯的第 4 步,會尋找函式宣告,值為函式體,也就是函式宣告會覆蓋變數宣告。

歡迎關注微信公眾號:前端閱讀室