深入理解JavaScript 變量對象

前言

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

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

  • 變量對象(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

今天我們來重點講解變量對象。

變量對象

變量對象是與執行上下文相關的數據作用域,存儲瞭在上下文中定義的變量和函數聲明。

執行上下文分為兩種:全局上下文和函數上下文,接下來我們來分別講解這兩種上下文的變量對象。

全局上下文中變量對象

全局上下文中的變量對象是全局對象。

下面我們來瞭解一下全局對象,在 W3school 中的介紹有:

  • 全局對象是預定義的對象,作為 JavaScript 的全局函數和全局屬性的占位符。通過使用全局對象,可以訪問所有其他預定義的對象、函數和屬性。

  • 在頂層 JavaScript 代碼中,可以用關鍵字 this 引用全局對象。全局對象在作用域鏈最底端,這意味著所有非限定性的變量和函數名都會作為該對象的屬性來查詢。

  • 由於全局對象在作用域鏈最底端,這也意味著在頂層 JavaScript 代碼中聲明的變量都將成為全局對象的屬性。

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

  • 在頂層 JavaScript 代碼中,可以用關鍵字 this 引用全局對象。在瀏覽器 JavaScript 中,全局對象是 window。在 node.js 中,全局對象是 global。
console.log(this); // window
console.log(this === window); // true
  • 全局對象是 JavaScript 的全局函數和全局屬性的占位符。在頂層 JavaScript 代碼中聲明的變量都將成為全局對象的屬性。
// 聲明的變量成為瞭全局對象的屬性
var a = 1;
console.log(this.a); // 1

// 聲明的函數成為瞭全局對象的屬性
function b() {}
console.log(this.b); // function b
  • 通過使用全局對象,可以訪問全局函數和全局屬性,也可以訪問所有其他預定義的對象、函數和屬性。
// 使用全局對象訪問全局屬性 Math,它是一個對象,它擁有 random 方法。
console.log(this.Math.random()); // 打印一個隨機數
  • 所有非限定性的變量和函數名都會作為該對象的屬性來查詢。
// 這裡的 Math 是非限定性的函數名
console.log(Math.random()); // 打印一個隨機數
  • 全局對象是 Object 構造函數的實例,這也意味著 Object.prototype(原型)上預定義的屬性和方法,是可以通過全局對象訪問到的。
console.log(this instanceof Object); // true
  • 在瀏覽器 JavaScript 中,全局對象有 window 屬性且指向自身。
console.log(this.window === this); // true

函數上下文中的變量對象

在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。

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

活動對象是在進入函數上下文時被創建的,它有函數的 arguments 屬性作為初始化屬性。arguments 屬性的值就是 Arguments 對象。

執行過程

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

預編譯

  • 創建 AO 對象,尋找形參和變量聲明

  • 把形參和變量名作為 AO 對象的屬性名,值為 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"
}

總結

至此,變量對象的創建過程我們就介紹完瞭,我們來做個總結:

  • 全局上下文的變量對象初始化是全局對象
  • 函數上下文的變量對象初始化隻包括 Arguments 對象
  • 在進入執行上下文時會給變量對象添加形參、變量聲明、函數聲明等初始的屬性值(預編譯)
  • 在代碼執行階段,會修改變量對象的屬性值

練習題

  • 第一題

來看下面兩端代碼,分別會打印什麼?

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

foo();
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。

  • 第二題
console.log(foo);

function foo() {}

var foo = 1;

會打印 foo 函數,而不是 undefined。

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

到此這篇關於深入理解JavaScript 變量對象的文章就介紹到這瞭,更多相關JavaScript 變量對象內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: