深刻JavaScript系列(一):詞法環境

1、詞法環境 (Lexical Environment)

ECMAScript規範中對詞法環境的描述以下:詞法環境是用來定義 基於詞法嵌套結構的ECMAScript代碼內的標識符與變量值和函數值之間的關聯關係 的一種規範類型。一個詞法環境由環境記錄(Environment Record)和一個可能爲null的對外部詞法環境的引用(outer)組成。通常來講,詞法環境都與特定的ECMAScript代碼語法結構相關聯,例如函數、代碼塊、TryCatch中的Catch從句,而且每次執行這類代碼時都會建立新的詞法環境。git

簡而言之,詞法環境就是相應代碼塊內標識符與值的關聯關係的體現。若是以前瞭解過做用域概念的話,和詞法環境是相似的(ES6以後做用域概念變爲詞法環境概念)。github

詞法環境有兩個組成部分:瀏覽器

  1. 環境記錄(Environment Record):記錄相應代碼塊的標識符綁定。函數

    能夠理解爲相應代碼塊內的全部變量聲明、函數聲明(代碼塊若爲函數還包括其形參)都儲存於此
    對應ES6以前的變量對象or活動對象,沒了解過的可忽略ui

  2. 對外部詞法環境的引用(outer):用於造成多個詞法環境在邏輯上的嵌套結構,以實現能夠訪問外部詞法環境變量的能力。this

    詞法環境在邏輯上的嵌套結構對應ES6以前的做用域鏈,沒了解過的可忽略spa

2、環境記錄(Environment Record)

環境記錄有三種類型,分別是聲明式環境記錄(Declarative Environment Record)對象式環境記錄(Object Environment Record)全局環境記錄(Global Environment Record)3d

1. 聲明式環境記錄(Declarative Environment Record)

聲明式環境記錄是用來定義那些直接將標識符與語言值綁定的ES語法元素,例如變量,常量,let,class,module,import以及函數聲明等。code

聲明式環境記錄有函數環境記錄(Function Environment Record)和模塊環境記錄(Module Environment Record)兩種特殊類型。cdn

1.1 函數環境記錄(Function Environment Record)

函數環境記錄用於體現一個函數的頂級做用域,若是函數不是箭頭函數,還會提供一個this的綁定。

1.2 模塊環境記錄(Module Environment Record)

模塊環境記錄用於體現一個模塊的外部做用域(即模塊export所在環境),除了正常綁定外,也提供了全部引入的其餘模塊的綁定(即import的全部模塊,這些綁定只讀),所以咱們能夠直接訪問引入的模塊。

2. 對象式環境記錄(Object Environment Record)

每一個對象式環境記錄都與一個對象相關聯,這個對象叫作對象式環境記錄的binding object。能夠理解爲對象式環境記錄就是基於這個binding object,以對象屬性的形式進行標識符綁定,標識符與binding object的屬性名一一對應。

是對象就能夠動態添加或者刪除屬性,因此對象環境記錄不存在不可變綁定。

對象式環境記錄用來定義那些將標識符與某些對象屬性相綁定的ES語法元素,例如with語句、全局var聲明和函數聲明。

3. 全局環境記錄(Global Environment Record)

全局環境記錄邏輯上來講是單個記錄,可是實際上能夠看做是對一個對象式環境記錄組件和一個聲明式環境記錄組件的封裝。

以前說過每一個對象式環境記錄都有一個binding object,全局環境記錄的對象式環境記錄binding object就是全局對象,在瀏覽器內,全局的thiswindow綁定都指向全局對象。

全局環境記錄的對象式環境記錄組件,綁定了全部內置全局屬性、全局的函數聲明以及全局的var聲明。

因此這些綁定咱們能夠經過window.xxthis.xx獲取到。

全局代碼的其餘聲明(如let、const、class等)則綁定在聲明式環境記錄組件內,因爲聲明式環境記錄組件並非基於簡單的對象形式來實現綁定,因此這些聲明咱們並不能經過全局對象的屬性來訪問

3、 外部詞法環境的引用(outer)

首先要說明兩點:

  1. 全局環境的外部詞法環境引用爲null
  2. 一個詞法環境能夠做爲多個詞法環境的外部環境。例如全局聲明瞭多個函數,則這些函數詞法環境的外部詞法環境引用都指向全局環境。

外部詞法環境的引用將一個詞法環境和其外部詞法環境連接起來,外部詞法環境又擁有對其自身的外部詞法環境的引用。這樣就造成一個鏈式結構,這裏咱們稱其爲環境鏈(即ES6以前的做用域鏈),全局環境是這條鏈的頂端。

環境鏈的存在是爲了標識符的解析,通俗的說就是查找變量。首先在當前環境查找變量,找不到就去外部環境找,還找不到就去外部環境的外部環境找,以此類推,直到找到,或者到環境鏈頂端(全局環境)還未找到則拋出ReferenceError

標識符解析:在環境鏈中解析變量(綁定)的過程,

咱們使用僞代碼來模擬一下標識符解析的過程。

ResolveBinding(name[, LexicalEnvironment]) {
    // 若是傳入詞法環境爲null(即一直解析到全局環境還未找到變量),則拋出ReferenceError
    if (LexicalEnvironment === null) {
        throw ReferenceError(`${name} is not defined`)
    }
    // 首次查找,將當前詞法環境設置爲解析環境
    if (typeof LexicalEnvironment === 'undefined') {
        LexicalEnvironment = currentLexicalEnvironment
    }
    // 檢查環境的環境記錄中是否有此綁定
    let isExist = LexicalEnvironment.EnviromentRecord.HasBinding(name)
    // 若是有則返回綁定值,沒有則去外層環境查找
    if (isExist) {
        return LexicalEnvironment.EnviromentRecord[name]
    } else {
        return ResolveBinding(name, LexicalEnvironment.outer)
    }
}
複製代碼

4、案例分析

上面講了那麼多理論知識,如今咱們結合代碼來複習,有如下全局代碼:

var x = 10
let y = 20
const z = 30
class Person {}
function foo() {
    var a = 10
}
foo()
複製代碼

如今咱們有了一個全局詞法環境和foo函數詞法環境(如下內容均爲抽象僞代碼):

// 全局詞法環境
GlobalEnvironment = {
    outer: null, // 全局環境的外部環境引用爲null
    // 全局環境記錄,抽象爲一個聲明式環境記錄和一個對象式環境記錄的封裝
    GlobalEnvironmentRecord: {
        // 全局this綁定值指向全局對象,即ObjectEnvironmentRecord的binding object
        [[GlobalThisValue]]: ObjectEnvironmentRecord[[BindingObject]],
        // 聲明式環境記錄,全局除了函數和var,其餘聲明綁定於此
        DeclarativeEnvironmentRecord: {
            y: 20,
            z: 30,
            Person: <<class>>
        },
        // 對象式環境記錄的,綁定對象爲全局對象,故其中的綁定能夠經過訪問全局對象的屬性來得到
        ObjectEnvironmentRecord: {
            // 全局函數聲明和var聲明
            x: 10,
            foo: <<function>>,
            // 內置全局屬性
            isNaN: <<function>>,
            isFinite: <<function>>,
            parseInt: <<function>>,
            parseFloat: <<function>>,
            Array: <<construct function>>,
            Object: <<construct function>>
            // 其餘內置全局屬性不一一列舉
        }
    }
}

// foo函數詞法環境
fooFunctionEnviroment = {
    outer: GlobalEnvironment, // 外部詞法環境引用指向全局環境
    FunctionEnvironmentRecord: {
        [[ThisValue]]: GlobalEnvironment, // foo函數全局調用,故this綁定指向全局環境
        // 其餘函數代碼內的綁定
        a: 10
    }
}
複製代碼

5、全局標識符解析

因爲全局環境記錄是聲明式環境記錄和對象式環境記錄的封裝,因此全局標識符的解析與其餘環境的標識符解析有所不一樣,下面介紹全局標識符解析的步驟(僞代碼):

function GetGlobalBingingValue(name) {
    // 全局環境記錄
    let rec = Global Environment Record
    // 全局環境記錄的聲明式環境記錄
    let DecRec = rec.DeclarativeRecord
    // HasBinding用來檢查環境記錄上是否綁定給定標識符
    if (DecRec.HasBinding(name) === true) {
        return DecRec[name]
    }
    let ObjRec = rec.ObjectRecord
    if (ObjRec.HasBinding(name) === true) {
        return ObjRec[name]
    }
    throw ReferenceError(`${name} is not defined`)
}
複製代碼

能夠看到讀取全局變量時,先檢索聲明式環境記錄,再檢索對象式環境記錄。這樣就會出現一些有趣的現象:

letconstclass等聲明的變量若是存在同名var變量或同名函數聲明,就會報錯(以後的文章中會具體介紹)。可是若是咱們使用letconstclass聲明變量,而後直接經過給全局對象添加一個同名屬性,則能夠繞過此類報錯。

此時全局環境記錄的聲明式環境記錄和對象式環境記錄內都有此標識符的綁定,可是咱們訪問時因爲先檢索聲明式環境記錄,因此對象式環境記錄內的綁定會被遮蔽,要想訪問只能經過訪問全局對象屬性的方法訪問。

系列文章

準備將以前寫的部分深刻ECMAScript文章重寫,加深本身理解,使內容更有乾貨,目錄結構也更合理。

深刻ECMAScript系列目錄地址(持續更新中...)

歡迎前往閱讀系列文章,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

菜鳥一枚,若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤,與你們共同進步。

相關文章
相關標籤/搜索