JavaScript系列之變量對象

JavaScript編程的時候總規避不了聲明變量和函數,可是解釋器是如何而且在什麼地方去查找這些變量和函數呢?接下來,再延續上一篇《JavaScript系列之執行上下文和執行棧》,經過對變量對象(Variable Object)的介紹對執行上下文有一個更深一步的瞭解。javascript

上一篇文章也提到了,一個執行上下文的生命週期能夠分爲三個階段:java

詳細瞭解執行上下文對於初學者來講極爲重要,由於其中涉及到了變量對象,做用域鏈,this等不少JavaScript初學者沒徹底搞懂,且極爲重要的概念,它關係到咱們能不能真正理解JavaScript,真正理解也能更爲輕鬆地勝任後續工做,在後面的文章中咱們會一一詳細介紹,這裏咱們先重點了解一下變量對象git

變量對象

變量對象(Variable Object)是一個與執行上下文相關的數據做用域,存儲了在上下文中定義的變量函數聲明,先來看一段代碼示例:github

function foo(){
    var a = 10;     
    function b(){}
    (function c(){});
    console.log(a);     // 10
    console.log(b);   // function b(){}
    console.log(c);   // Uncaught ReferenceError: c is not defined
}

foo();
複製代碼

在上面的例子中,foo()函數的變量對象包含變量a函數b()的聲明。這裏要注意的一點是,函數表達式並不像函數聲明同樣包含在變量對象中,在示例中所看到的那樣,訪問c()函數會致使引用錯誤。由於變量對象是抽象的和特殊的,它不能在代碼中訪問,但會由JavaScript引擎處理。express

上面利用的是函數上下文下的變量對象來講明變量對象儲存了什麼,但變量對象還存在於全局上下文中,接下來就分別來聊聊全局上下文中和函數上下文中的變量對象吧。編程

全局上下文

以瀏覽器中爲例,全局對象爲window。 全局上下文有一個特殊的地方,它的變量對象,就是window全局對象。而這個特殊,在this指向上也一樣適用,this也是指向window數組

// 以瀏覽器中爲例,全局對象爲window
// 全局上下文建立階段
// VO 爲變量對象(Variable Object)的縮寫
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}
複製代碼

除此以外,全局上下文的生命週期,與程序的生命週期一致,只要程序運行不結束,好比關掉瀏覽器窗口,全局上下文就會一直存在。其餘全部的上下文環境,都能直接訪問全局上下文的屬性。瀏覽器

函數上下文

在上面已經提到了,變量對象存儲了執行上下文中的變量和函數聲明,但在函數上下文中,還多了一個arguments(函數參數列表), 一個僞數組對象。函數

這時變量對象的建立階段會包括:ui

  1. 建立arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。
  2. 檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性。
  3. 檢查當前上下文中的變量聲明var 聲明的變量),默認爲 undefined;若是變量名稱跟已經聲明的形式參數或函數相同,爲了防止同名的函數被修改成undefined,則會直接跳過變量聲明,原屬性值不會被修改。

對於第3點中的「跳過」一詞想必你們會有一絲疑問?底下例子中既然按照上面的規則,變量聲明的foo遇到函數聲明的foo會跳過,但是爲何最後foo的輸出結果仍然是被覆蓋了?

function foo() { console.log('I am function foo') }
var foo = 10;

console.log(foo); // 10
複製代碼

理由其實很簡單,由於上面的三條規則僅僅適用於變量對象的建立過程,也就是執行上下文的建立過程。而foo = 10是在執行上下文的執行過程中運行的,輸出結果天然會是10。對比下例:

console.log(foo); // ƒ foo() { console.log('I am function foo') }
function foo() { console.log('I am function foo') }

var foo = 10;
console.log(foo); // 10
複製代碼

爲啥又是不同的結果呢?其實它的執行順序爲:

// 首先將全部函數聲明放入變量對象中,函數聲明變量提高
function foo() { console.log('I am function foo') }

// 其次將全部變量聲明放入變量對象中,可是由於foo已經存在同名函數,所以此時會跳過變量聲明默認undefined的賦值
// var foo = undefined;

// 而後開始執行階段代碼的執行
console.log(foo); // ƒ foo() { console.log('I am function foo') }

// 在執行上下文的執行過程當中運行
foo = 10;
console.log(foo); // 10
複製代碼

根據上面的規則,理解變量提高就變得十分簡單了,咱們也能夠看出,function聲明會比var聲明優先級更高一點。爲了幫助你們更好的理解變量對象,咱們再結合一個簡單的例子來進行探討。

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

    var a = 1;
    function foo() {
        return 2;
    }
}

test();

/* 結果爲: undefined 2 */
複製代碼

根據上述的規則,理解變量提高後能夠將執行順序理解爲:

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;
}

test();
複製代碼

這樣是否是一目瞭然了呢?

固然還須要注意的是,函數未進入執行階段以前,變量對象中的屬性都不能訪問!可是進入執行階段以後,變量對象(VO)轉變爲了活動對象(AO),而後開始進行執行階段的操做。

執行階段

當前進入執行階段,變量對象(VO)激活成活動對象(AO),裏面的屬性都能被訪問了,函數會順序執行代碼,改變變量對象的屬性值,此階段的執行上下文代碼會分紅兩個階段進行處理:

  1. 進入執行上下文
  2. 執行代碼

進入執行上下文

當進入執行上下文時,這時候尚未執行代碼。讓咱們看一個例子:

function foo(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}
  
foo(10); 
複製代碼

當進入帶有參數10的foo函數上下文時,AO表現爲以下:

AO = {
    arguments: {
        0: 10,
        1: undefined,
        length: 1
    }
    a: 10,
    b: undefined,
    c: undefined,
    d: <function reference to d>, e: undefined, } 複製代碼

x 是函數表達式,因此不在變量對象當中,e 變量引用的值也是函數表達式,因此變量 e 自己是聲明,因此在變量對象當中。

執行代碼

這個階段會按順序執行代碼,修改變量對象的屬性值,緊接上面的例子,執行完成後AO以下:

AO = {
    arguments: {
        0: 10,
        1: undefined,
        length: 1
    }
    a: 10,
    b: undefined,
    c: 10,
    d: <reference to function declaration d>,
    e: <reference to Function expression to _e>,
}
複製代碼

到這裏變量對象的建立過程就介紹完了,讓咱們簡短地總結一下:

  1. 全局上下文的變量對象初始化是全局對象
  2. 函數上下文的變量對象初始化只包括 Arguments 對象
  3. 在進入執行上下文時會依次給變量對象添加形參函數聲明變量聲明等初始的屬性值
  4. 函數未進入執行階段以前,變量對象中的屬性都不能訪問
  5. 在執行代碼階段,會再次修改變量對象的屬性值,並賦予該有的屬性值

若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!

相關文章
相關標籤/搜索