前端基礎進階(三):變量對象詳解

開年以後工做熱情一直不是很高,這幾天一直處於消極怠工狀態。早上不想起牀,起牀了不想上班。明明放假以前工做熱情還一直很高,一直心心念唸的想把小程序項目懟出來,結果休假回來以後畫風徹底不同了。我感受本身得了嚴重了節後綜合徵。還好擼了幾篇文章,勉強表示這一週的時間沒有徹底浪費。這篇文章要給你們介紹的是變量對象。javascript

在JavaScript中,咱們確定不可避免的須要聲明變量和函數,但是JS解析器是如何找到這些變量的呢?咱們還得對執行上下文有一個進一步的瞭解。前端

在上一篇文章中,咱們已經知道,當調用一個函數時(激活),一個新的執行上下文就會被建立。而一個執行上下文的生命週期能夠分爲兩個階段。java

  • 建立階段

在這個階段中,執行上下文會分別建立變量對象,創建做用域鏈,以及肯定this的指向。面試

  • 代碼執行階段

建立完成以後,就會開始執行代碼,這個時候,會完成變量賦值,函數引用,以及執行其餘代碼。小程序

執行上下文生命週期

從這裏咱們就能夠看出詳細瞭解執行上下文極爲重要,由於其中涉及到了變量對象,做用域鏈,this等不少人沒有怎麼弄明白,可是卻極爲重要的概念,它關係到咱們能不能真正理解JavaScript。在後面的文章中咱們會一一詳細總結,這裏咱們先重點了解變量對象。segmentfault

變量對象(Variable Object)

變量對象的建立,依次經歷瞭如下幾個過程。瀏覽器

  1. 創建arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。
  2. 檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是函數名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
  3. 檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名創建一個屬性,屬性值爲undefined。若是該變量名的屬性已經存在,爲了防止同名的函數被修改成undefined,則會直接跳過,原屬性值不會被修改。

許多讀者在閱讀到這的時候會由於下面的這樣場景對於「跳過」一詞產生疑問。既然變量聲明的foo遇到函數聲明的foo會跳過,但是爲何最後foo的輸出結果仍然是被覆蓋了?函數

function foo() { console.log('function foo') }
var foo = 20;

console.log(foo); // 20

其實只是你們在閱讀的時候不夠仔細,由於上面的三條規則僅僅適用於變量對象的建立過程。也就是執行上下文的建立過程。而foo = 20是在執行上下文的執行過程當中運行的,輸出結果天然會是20。對比下例。this

console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
// 上慄的執行順序爲

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

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

// 而後開始執行階段代碼的執行
console.log(foo); // function foo
foo = 20;

我知道有的人不喜歡看文字

根據這個規則,理解變量提高就變得十分簡單了。在不少文章中雖然提到了變量提高,可是具體是怎麼回事還真的不少人都說不出來,之後在面試中用變量對象的建立過程跟面試官解釋變量提高,保證瞬間提高逼格。spa

在上面的規則中咱們看出,function聲明會比var聲明優先級更高一點。爲了幫助你們更好的理解變量對象,咱們結合一些簡單的例子來進行探討。

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

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

test();

在上例中,咱們直接從test()的執行上下文開始理解。全局做用域中運行test()時,test()的執行上下文開始建立。爲了便於理解,咱們用以下的形式來表示

// 建立過程
testEC = {
    // 變量對象
    VO: {},
    scopeChain: {}
}

// 由於本文暫時不詳細解釋做用域鏈,因此把變量對象專門提出來講明

// VO 爲 Variable Object的縮寫,即變量對象
VO = {
    arguments: {...},  //注:在瀏覽器的展現中,函數的參數可能並非放在arguments對象中,這裏爲了方便理解,我作了這樣的處理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}

未進入執行階段以前,變量對象中的屬性都不能訪問!可是進入執行階段以後,變量對象轉變爲了活動對象,裏面的屬性都能被訪問了,而後開始進行執行階段的操做。

這樣,若是再面試的時候被問到變量對象和活動對象有什麼區別,就又能夠自如的應答了,他們其實都是同一個對象,只是處於執行上下文的不一樣生命週期。不過只有處於函數調用棧棧頂的執行上下文中的變量對象,纔會變成活動對象。
// 執行階段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}

所以,上面的例子demo1,執行順序就變成了這樣

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

test();

再來一個例子,鞏固一下咱們的理解。

// demo2
function test() {
    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}

test();
// 建立階段
VO = {
    arguments: {...},
    foo: <foo reference>,
    bar: undefined
}
// 這裏有一個須要注意的地方,由於var聲明的變量當遇到同名的屬性時,會跳過而不會覆蓋
// 執行階段
VO -> AO
VO = {
    arguments: {...},
    foo: 'Hello',
    bar: <bar reference>,
    this: Window
}

須要結合上面的知識,仔細對比這個例子中變量對象從建立階段到執行階段的變化,若是你已經理解了,說明變量對象相關的東西都已經難不倒你了。

全局上下文的變量對象

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

// 以瀏覽器中爲例,全局對象爲window
// 全局上下文
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}

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

前端基礎進階系列目錄

clipboard.png

相關文章
相關標籤/搜索