理解 ECMAScript 規範,第2部分
讓我們來多練習一些我們驚人的規範閱讀技巧。如果你還沒看過上一集,現在是個好機會!
準備好進入第2部分了嗎?
了解規範的一個有趣的方法是從我們已知的 JavaScript 功能入手,找出它是如何被規範定義的。
警告!本集包含來自 ECMAScript 規範(截至 2020 年 2 月)的複製粘貼算法,最終它們可能會過時。
我們知道屬性會在原型鏈中查找:如果一個物件沒有我們試圖讀取的屬性,我們會沿著原型鏈往上找到它(或找到一個不再有原型的物件)。
例如:
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
原型鏈的查找定義在哪裡?
讓我們嘗試找出這種行為的定義位置。一個不錯的起點是 物件內部方法清單。
這裡既有 [[GetOwnProperty]] 也有 [[Get]] ——我們關心的是不限於 自有 屬性的版本,所以我們選擇 [[Get]]。
不幸的是,屬性描述符的規範類型 也有一個名為 [[Get]] 的欄位,因此在瀏覽規範時,我們需要小心區分這兩種不同的用法。
[[Get]] 是一個 基本內部方法。普通物件實現基本內部方法的預設行為。特殊物件可以定義他們自己的 [[Get]] 方法,與預設行為不同。在這篇文章中,我們專注於普通物件。
[[Get]] 的預設實作委派給 OrdinaryGet:
當
O的[[Get]]內部方法以屬性鍵P和 ECMAScript 值Receiver被呼叫時,按照以下步驟執行:
- 返回
? OrdinaryGet(O, P, Receiver)。
我們很快會看到,Receiver 是在調用訪問器屬性(accessor property)中的 getter 函數時作為 this 值 使用的值。
OrdinaryGet 的定義如下:
OrdinaryGet ( O, P, Receiver )當抽象操作
OrdinaryGet以物件O,屬性鍵P和 ECMAScript 值Receiver被呼叫時,按照以下步驟執行:
- 斷言:
IsPropertyKey(P)是true。- 令
desc為? O.[[GetOwnProperty]](P)。- 如果
desc是undefined,則
- 令
parent為? O.[[GetPrototypeOf]]()。- 如果
parent是null,返回undefined。- 返回
? parent.[[Get]](P, Receiver)。- 如果
IsDataDescriptor(desc)是true,返回desc.[[Value]]。- 斷言:
IsAccessorDescriptor(desc)是true。- 令
getter為desc.[[Get]]。- 如果
getter是undefined,返回undefined。- 返回
? Call(getter, Receiver)。
原型鏈的查找在第 3 步:如果我們沒有找到該屬性作為自有屬性,我們調用原型的 [[Get]] 方法,該方法再次委派給 OrdinaryGet。如果還是找不到,我們繼續調用其原型的 [[Get]] 方法,依次類推,直到找到該屬性或到達一個沒有原型的物件。
讓我們看看當我們訪問 o2.foo 時該算法如何工作。我們首先以 O 為 o2,P 為 "foo" 調用 OrdinaryGet。O.[[GetOwnProperty]]("foo") 返回 undefined,因為 o2 沒有名為 "foo" 的自有屬性,所以我們進入第 3 步的 if 分支。在第 3.a 步中,我們將 parent 設置為 o2 的原型,即 o1。parent 不是 null,所以我們不在第 3.b 步返回。在第 3.c 步中,我們以屬性鍵 "foo" 調用父物件的 [[Get]] 方法,並返回它的結果。
父物件(o1)是一個普通物件,所以其 [[Get]] 方法再次調用 OrdinaryGet,此時以 O 為 o1 和 P 為 "foo"。o1 有一個名為 "foo" 的自有屬性,因此在第 2 步中,O.[[GetOwnProperty]]("foo") 返回相關的屬性描述符,我們將其存儲在 desc 中。
財產描述符是一種規範型別。資料屬性描述符直接將屬性值儲存在[[Value]]欄位。訪問器屬性描述符將訪問器函數儲存在[[Get]]和/或[[Set]]欄位中。在這種情況下,與"foo"相關聯的財產描述符是一個資料屬性描述符。
我們在步驟2中儲存在desc中的資料屬性描述符不是undefined,因此我們不會執行步驟3中的if分支。接下來,我們執行步驟4。財產描述符是一個資料屬性描述符,因此我們在步驟4中返回其[[Value]]欄位99,完成操作。
Receiver是什麼以及從哪裡來?
Receiver參數僅在步驟8中的訪問器屬性情況下使用。當調用訪問器屬性中的getter函數時,它作為this值傳遞。
OrdinaryGet在整個遞歸過程中保持原始Receiver不變(步驟3.c)。讓我們找出Receiver最初是從哪裡來的!
搜尋[[Get]]被調用的地方,我們找到了一個作用於References的抽象操作GetValue。Reference是一種規範型別,由基值、引用名稱和嚴格引用標誌組成。在o2.foo的情況下,基值是Object o2,引用名稱是字串"foo",而嚴格引用標誌是false,因為示例代碼是鬆散的。
插曲:為什麼Reference不是記錄(Record)?
插曲:Reference不是記錄(Record),儘管看起來它可以是。它包含三個組件,這三個組件完全可以表述為三個命名欄位。Reference不是記錄只是因為歷史原因。
回到GetValue
讓我們看看GetValue是如何定義的:
ReturnIfAbrupt(V)。- If
Type(V)is notReference, returnV。- Let
basebeGetBase(V)。- If
IsUnresolvableReference(V)istrue, throw aReferenceErrorexception。- If
IsPropertyReference(V)istrue, then
- If
HasPrimitiveBase(V)istrue, then
- Assert: In this case,
basewill never beundefinedornull。- Set
baseto! ToObject(base)。- Return
? base.[[Get]](GetReferencedName(V), GetThisValue(V))。- Else,
- Assert:
baseis an Environment Record。- Return
? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
我們示例中的Reference是o2.foo,它是一個財產引用(Property Reference)。因此,我們採取分支5。我們不採取分支5.a,因為基值o2不是原始值(如Number、String、Symbol、BigInt、Boolean、Undefined或Null)。
然後我們在步驟5.b中調用[[Get]]。我們傳遞的Receiver是GetThisValue(V)。在這種情況下,它只是Reference的基值:
- Assert:
IsPropertyReference(V)istrue。- If
IsSuperReference(V)istrue, then
- Return the value of the
thisValuecomponent of the referenceV。- Return
GetBase(V)。
對於o2.foo,我們不採取步驟2中的分支,因為它不是超級引用(如super.foo),但我們採取步驟3並返回Reference的基值o2。
將所有東西拼湊在一起,我們發現我們將Receiver設置為原始Reference的基值,然後在原型鏈遍歷過程中保持其不變。最後,如果我們找到的屬性是一個訪問器屬性,我們使用Receiver作為調用它的this值。
特別是,getter中的this值指的是我們嘗試獲取屬性所在的原始對象,而不是我們在原型鏈遍歷過程中找到屬性所在的對象。
讓我們試試看!
const o1 = { x: 10, get foo() { return this.x; } };
const o2 = { x: 50 };
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 50
在此示例中,我們有一個名為foo的訪問器屬性,我們為其定義了一個getter。getter返回this.x。
然後我們訪問o2.foo——getter返回什麼?
我們發現當我們調用getter時,this值是我們最初嘗試獲取屬性所在的對象,而不是我們在原型鏈遍歷過程中找到屬性所在的對象。在此情況下,this值是o2,而不是o1。我們可以通過檢查getter是返回o2.x還是o1.x來驗證,事實上,它返回的是o2.x。
成功了!我們能夠根據規範中讀到的內容預測此代碼片段的行為。
訪問屬性 — 為什麼它會調用[[Get]]?
規範在哪裡說當訪問屬性如o2.foo時,Object內部方法[[Get]]會被調用?這一定在某處有所定義。別只聽我的說法!
我們發現Object內部方法[[Get]]是從操作於References的抽象操作GetValue中調用的。但GetValue是從哪裡調用的?
MemberExpression的執行語義
規範的語法規則定義了語言的語法。執行時語意定義了語法構造的「含義」(如何在執行時進行評估)。
如果你不熟悉上下文無關文法,現在看看是個好主意!
我們将在後續章節中深入探討語法規則,目前我們保持簡單!尤其是,對於本章,我們可以忽略生成式中的下標(Yield、Await等)。
以下生成式描述了MemberExpression的結構:
MemberExpression :
PrimaryExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
MemberExpression TemplateLiteral
SuperProperty
MetaProperty
new MemberExpression Arguments
這裡我們有7個MemberExpression的生成式。一個MemberExpression可以僅僅是一個PrimaryExpression。或者可以從另一個MemberExpression和Expression通過拼接它們來構造,例如MemberExpression [ Expression ],如o2['foo']。或者可以是MemberExpression . IdentifierName,如o2.foo——這是與我們示例相關的生成式。
生成式MemberExpression : MemberExpression . IdentifierName的執行時語意定義了在評估它時需要執行的步驟:
執行時語意:針對
MemberExpression : MemberExpression . IdentifierName的評估
- 令
baseReference為評估MemberExpression的結果。- 令
baseValue為? GetValue(baseReference)。- 如果此
MemberExpression匹配的代碼是嚴格模式代碼,則令strict為true;否則令strict為false。- 返回
? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict)。
該算法委托給抽象操作EvaluatePropertyAccessWithIdentifierKey,因此我們也需要閱讀它:
EvaluatePropertyAccessWithIdentifierKey(baseValue, identifierName, strict)抽象操作
EvaluatePropertyAccessWithIdentifierKey將baseValue作為值,identifierName作為解析節點,以及strict作為布林參數。它執行以下步驟:
- 斷言:
identifierName是IdentifierName。- 令
bv為? RequireObjectCoercible(baseValue)。- 令
propertyNameString為identifierName的StringValue。- 返回一個類型為引用的值,其基值組件為
bv,其被引用的名稱組件為propertyNameString,其嚴格引用標記為strict。
也就是說:EvaluatePropertyAccessWithIdentifierKey構造了一個引用,該引用使用提供的baseValue作為基值,使用identifierName的字符串值作為屬性名稱,並使用strict作為嚴格模式標誌。
最終,該引用被傳遞給GetValue。這在規範的多個地方有定義,具體依賴於引用最終的使用方式。
MemberExpression作為參數
在我們的示例中,我們使用屬性訪問作為參數:
console.log(o2.foo);
在這種情況下,行為在ArgumentList生成式的執行時語意中進行定義,該語意調用了該參數的GetValue:
ArgumentList : AssignmentExpression
- 令
ref為評估AssignmentExpression的結果。- 令
arg為? GetValue(ref)。- 返回唯一項
arg的列表。
o2.foo看起來不像AssignmentExpression,但它正是如此,所以此生成式適用。要了解原因,你可以查看這個額外內容,但目前這不是絕對必要的。
第1步中的AssignmentExpression是o2.foo。ref是評估o2.foo的結果,即上述提到的引用。在第2步中,我們在其上調用GetValue。因此,我們知道內部方法[[Get]]將被調用,原型鏈遍歷將會發生。
總結
在本章中,我們研究了規範如何在不同層次定義語言特性,此處是原型查找,包括觸發該特性的語法構造和定義它的算法。