一、詞法環境 (Lexical Environment)
ECMAScript規範中對詞法環境的描述如下:詞法環境是用來定義 基於詞法巢狀結構的ECMAScript程式碼內的識別符號與變數值和函式值之間的關聯關係 的一種規範型別。一個詞法環境由環境記錄(Environment Record)和一個可能為
null
的對外部詞法環境的引用(outer)組成。一般來說,詞法環境都與特定的ECMAScript程式碼語法結構相關聯,例如函式、程式碼塊、TryCatch
中的Catch
從句,並且每次執行這類程式碼時都會建立新的詞法環境。
簡而言之,詞法環境就是相應程式碼塊內識別符號與值的關聯關係的體現。如果之前瞭解過作用域概念的話,和詞法環境是類似的(ES6之後作用域概念變為詞法環境概念)。
詞法環境有兩個組成部分:
-
環境記錄(Environment Record):記錄相應程式碼塊的識別符號繫結。
可以理解為相應程式碼塊內的所有變數宣告、函式宣告(程式碼塊若為函式還包括其形參)都儲存於此
對應ES6之前的變數物件or活動物件,沒了解過的可忽略 -
對外部詞法環境的引用(outer):用於形成多個詞法環境在邏輯上的巢狀結構,以實現可以訪問外部詞法環境變數的能力。
詞法環境在邏輯上的巢狀結構對應ES6之前的作用域鏈,沒了解過的可忽略
二、環境記錄(Environment Record)
環境記錄有三種型別,分別是宣告式環境記錄(Declarative Environment Record)、物件式環境記錄(Object Environment Record)、全域性環境記錄(Global Environment Record)。
1. 宣告式環境記錄(Declarative Environment Record)
宣告式環境記錄是用來定義那些直接將識別符號與語言值繫結的ES語法元素,例如變數,常量,let,class,module,import以及函式宣告等。
宣告式環境記錄有函式環境記錄(Function Environment Record)和模組環境記錄(Module Environment Record)兩種特殊型別。
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
就是全域性物件,在瀏覽器內,全域性的this
及window
繫結都指向全域性物件。
全域性環境記錄的物件式環境記錄
元件,繫結了所有內建全域性屬性、全域性的函式宣告以及全域性的var
宣告。
所以這些繫結我們可以通過window.xx
或this.xx
獲取到。
全域性程式碼的其他宣告(如let、const、class等)則繫結在宣告式環境記錄
元件內,由於宣告式環境記錄
元件並不是基於簡單的物件形式來實現繫結,所以這些宣告我們並不能通過全域性物件的屬性來訪問。
三、 外部詞法環境的引用(outer)
首先要說明兩點:
- 全域性環境的外部詞法環境引用為
null
。 - 一個詞法環境可以作為多個詞法環境的外部環境。例如全域性宣告瞭多個函式,則這些函式詞法環境的外部詞法環境引用都指向全域性環境。
外部詞法環境的引用將一個詞法環境和其外部詞法環境連結起來,外部詞法環境又擁有對其自身的外部詞法環境的引用。這樣就形成一個鏈式結構,這裡我們稱其為環境鏈(即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)
}
}
複製程式碼
四、案例分析
上面講了那麼多理論知識,現在我們結合程式碼來複習,有以下全域性程式碼:
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
}
}
複製程式碼
五、全域性識別符號解析
由於全域性環境記錄是宣告式環境記錄和物件式環境記錄的封裝,所以全域性識別符號的解析與其他環境的識別符號解析有所不同,下面介紹全域性識別符號解析的步驟(虛擬碼):
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`)
}
複製程式碼
可以看到讀取全域性變數時,先檢索宣告式環境記錄,再檢索物件式環境記錄。這樣就會出現一些有趣的現象:
let
、const
、class
等宣告的變數如果存在同名var
變數或同名函式宣告,就會報錯(之後的文章中會具體介紹)。但是如果我們使用let
、const
、class
宣告變數,然後直接通過給全域性物件新增一個同名屬性,則可以繞過此類報錯。
此時全域性環境記錄的宣告式環境記錄和物件式環境記錄內都有此識別符號的繫結,但是我們訪問時由於先檢索宣告式環境記錄,所以物件式環境記錄內的繫結會被遮蔽,要想訪問只能通過訪問全域性物件屬性的方法訪問。
系列文章
準備將之前寫的部分深入ECMAScript文章重寫,加深自己理解,使內容更有乾貨,目錄結構也更合理。
歡迎前往閱讀系列文章,如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
菜鳥一枚,如果有疑問或者發現錯誤,可以在相應的 issues 進行提問或勘誤,與大家共同進步。