講清楚之 javascript 變量對象

變量對象

這一節聊一下變量對象。都是乾貨(^▽^)javascript

變量對象是函數運行時數據的集合,存儲了在上下文中定義的變量和函數,不一樣的函數的變量對象稍有不一樣。java

仍是從上下文提及,javascript 引擎執行到函數的時候會向上下文棧中壓入一個上下文。
上下文中包含:es6

name -
變量對象(VO, variable object) 當前函數定義的變量、函數、參數
做用域鏈(Scope chain) 源代碼定義時造成的做用域鏈
this

僞代碼:瀏覽器

// 全局上下文的僞代碼
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}
  • 做用域鏈爲當前函數提供上層可訪問變量和函數的有序集合;
  • this爲函數提供運行時對象環境;
  • 變量對象提供當前函數定義時的變量和函數;

上下文生成時包含的做用域鏈的造成,和this的指向原則在前面的章節已經梳理過。
javascript 中主要有全局上下文、函數上下文。先了解一下函數調用時上下文棧的變化。函數

上下文棧狀態

當函數被調用的時候,一個新的上下文會被加入到上下文棧的頂部, 然後會運行函數內部的代碼塊。this

因此一個執行上下文或者說函數的生命週期分爲上下文建立階段、函數運行階段,不一樣階段上下文棧狀態和變量對象屬性會不同。spa

示例代碼:code

function foo (a) {
    var b = 1
    function c () {
        console.log(a + b) // 100
    }
    c()
}
foo(99)

執行上下文建立階段

在這個階段上下文對象會生成,並建立變量對象、建立做用域鏈、肯定 this 的指向。對象

說一千道一萬還不如來張圖乾脆。blog

foo 函數上下文建立完成後的棧狀態示意圖:

圖片描述

注意:此時函數內表達式和語句未執行,變量對象屬性值是根據規則被設置爲初始值的。

運行階段

上下文生成完成後,進入函數運行階段。依次按照代碼順序執行函數內代碼(變量的賦值、表達式計算、語句的執行,其餘函數調用等)。

在該階段會訪問或設置變量對象(活動對象)屬性的值。當執行完 foo 函數內第一行代碼var b = 1,此時棧狀態:

圖片描述

以此類推,每次函數表達式執行時都會在執行上下文長中獲取標識符的值,經過運算後又將結果保存在指定的標識符裏。所以執行上下文爲函數提供一個相似於寄存器的概念來管理數據的功能。

當函數執行完後,對應的執行上下文被銷燬。JavaScript 執行器會返回父函數或依據源代碼順序跳轉到其餘函數。

函數上下文

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

活動對象和變量對象實際上是一個東西,只是變量對象是規範上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象纔會被激活,因此才叫 activation object ,而只有被激活的變量對象,也就是活動對象上的各類屬性才能被訪問。

活動對象也是在進入函數上下文時刻被建立的,活動對象變量對象的一種激活狀態。因此當你把變量對象活動對象記混淆了沒關係,由於他們本質上對咱們理解函數調用時的細節沒有影響。我在大多數時候也直接用變量對象來表述。

變量對象的建立

變量對象的建立主要是進行標識符值類型的申明和初始化,聽從下面這3條原則:

  1. 生成 arguments 對象。檢查當前上下文的形參,生成屬性與屬性值對象(key: value)

    • 當形參沒有被賦值時, 屬性值被設置爲 undefined
  2. 在變量對象上創建函數索引。檢查當前做用域定義的 function,在變量對象中以函數名爲 key, 以函數所在內存地址爲 value 創建索引。

    • 若是函數名已經在變量對象中,則該函數名對應的函數會被新的函數替換。因此函數能夠被重複定義,後定義的函數會覆蓋掉先前定義的。
  3. 申明變量。檢查當前做用域定義的變量,在變量對象中以變量名爲 key, 以 undefined 爲值掛載內部變量。

    • 若是新申明的變量名與已經申明的形參名、函數名相同,則申明會被拋棄。

因此須要注意的是函數申明比變量申明優先級高,一旦函數申明佔用了某一個標識符,後續的變量申明若是使用的是先前使用過的函數標識符, 則該變量申明無效。

栗子1:

function foo () {
    function too() {
    }
    var too
    console.log(typeof too)
}
foo()
// function
// 變量 too 的申明無效

咱們把上面的栗子稍做修改, 栗子2:

function foo () {
    function too() {
    }
    var too = 1
    console.log(typeof too)
}
foo()
// number
// 變量 too 的值類型爲 number

變量 too 的值類型爲 number, 看到這個栗子你們可能會疑惑, 由於根據上面的3條規則, too 的第二次申明應該是無效的且 too 的類型應該爲 function。 其實 too 的值類型在上下文建立階段確實是 function, 因爲 javascript 是動態弱類型語言, 在上下文執行階段 var too = 1 實質是在給 too 賦值而且發生了隱式類型轉換, 因此在執行階段 too 變成了 number 類型。es6 語法中已經不建議使用var 來申明變量了, 而是使用let 來申明局部變量,從語法層面強制避免了重複的變量申明, 這樣栗子2中的狀況會直接報錯。

將上面的栗子再次修改,進一步探索:

function foo () {
    function too() {
    }
    console.log(typeof too) // function
    var too = 1
    console.log(typeof too) // number
}
foo()

foo 函數運行時會先打印 ‘function’,而後打印 ‘number’。首先表達式console.log(typeof too)執行時標識符too在上下文建立階段被初始化爲一個函數。var too = 1執行後標識符too被賦值爲 1,因此第二次console.log(typeof too)的時候輸出的是number.

再再舉一個例子:

function foo () {
    console.log(a)
    console.log(bar)
    var a = 1
    function bar() {
        return 2
    }
}
foo()
// undefind
// ƒ bar() {
//      return 2
//  }

上下文建立階段解析函數內代碼塊後,會在變量對象上添加 ‘a’, ‘bar’ 兩個標識符,並填充相應的值結束上下文的建立階段進入foo 函數的執行階段。

在執行階段 foo 函數體第一行表達式要求打印輸出 a 的值, 因爲 console.log(a) 以前沒有對 a 進行任何賦值操做,根據規則此時 a 的值爲 undefind 因此輸出 'undefind'。函數體內第二行要求打印輸出 bar 的值,根據規則標識符 'bar' 對應的是函數,因此 'bar' 的值爲函數實體,且在 console.log(bar) 以前也未對 bar 作賦值操做,因此打印出來的是該函數。

變量對象建立完,函數運行前的變量對象是這樣的:

// VO 爲 Variable Object的縮寫,即變量對象
VO = {
    arguments: {...},
    bar: <bar reference>  // 表示bar的地址引用
    a: undefined
}

變量對象(活動對象)的建立過程實質上就是咱們常常提起的函數變量提高,這裏3條原則纔是變量提高的本質。

若是沒有理解透徹能夠回頭看看前面的內容。

全局上下文

在瀏覽器中,全局對象就是 window ,也是瀏覽器提供的預約義變量對象,能夠經過 thisself 引用。全局對象提供瀏覽器預置對象 Array、Object、console.log、alert 等,全局上下文的生命週期和函數的生命週期同樣,只要程序運行不結束全局上下文就一直存在。其餘全部的上下文環境,都能直接訪問全局上下文的屬性。

全局對象是預約義的對象,做爲 JavaScript 的全局函數和全局屬性的佔位符。經過使用全局對象,能夠訪問全部其餘全部預約義的對象、函數和屬性。在頂層 JavaScript 代碼中,能夠用關鍵字 this 引用全局對象。由於全局對象是做用域鏈的頭(最外層做用域),這意味着全部非限定性的變量和函數名都會做爲該對象的屬性來查詢。例如,當JavaScript 代碼引用 parseInt() 函數時,它引用的是全局對象的 parseInt 屬性。全局對象是做用域鏈的頭,還意味着在頂層 JavaScript 代碼中聲明的全部變量都將成爲全局對象的屬性。

經過thisseif訪問全局對象:

console.log(this)
console.log(self)
相關文章
相關標籤/搜索