理解 ECMAScript 規範,第一部分
在本文中,我們採用規範中的一個簡單函數,並嘗試理解其標記法。讓我們開始吧!
前言
即使您了解 JavaScript,也可能會覺得閱讀其語言規範,ECMAScript 語言規範,或簡稱 ECMAScript 規範,相當令人生畏。至少這是我第一次閱讀時的感受。
讓我們從一個具體的例子開始,並逐步了解規範。以下代碼演示了 Object.prototype.hasOwnProperty 的使用:
const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
在這個例子中,o 沒有一個名為 hasOwnProperty 的屬性,所以我們沿著原型鏈查找。我們在 o 的原型中找到了它,這就是 Object.prototype。
為了描述 Object.prototype.hasOwnProperty 的工作原理,規範使用類似偽代碼的描述:
Object.prototype.hasOwnProperty(V)當
hasOwnProperty方法使用參數V調用時,按以下步驟執行:
- 設置
P為? ToPropertyKey(V)。- 設置
O為? ToObject(this value)。- 返回
? HasOwnProperty(O, P)。
…和…
抽象操作
HasOwnProperty用於確定某個對象是否具有指定屬性鍵的自身屬性。返回一個布林值。該操作使用參數O和P調用,其中O是對象,P是屬性鍵。該抽象操作執行以下步驟:
- 斷言:
Type(O)是Object。- 斷言:
IsPropertyKey(P)是true。- 設置
desc為? O.[[GetOwnProperty]](P)。- 如果
desc是undefined,返回false。- 返回
true。
但什麼是 “抽象操作”? [[ ]] 裡面的東西是什麼?為什麼函數前面有一個 ??斷言意味著什麼?
讓我們來探討一下!
語言類型與規範類型
讓我們從看起來熟悉的東西開始。規範使用像 undefined、true 和 false 這樣的值,那些值我們已經從 JavaScript 中了解。它們都是 語言值,是規範也定義的 語言類型 的值。
規範在內部也使用語言值,例如,內部數據類型可能包含一個值字段,其可能值為 true 和 false。相比之下,JavaScript 引擎通常不在內部使用語言值。例如,如果 JavaScript 引擎是用 C++ 編寫的,它通常會使用 C++ 的 true 和 false(而不是其 JavaScript 的 true 和 false 的內部表示)。
除了語言類型之外,規範還使用 規範類型,這些類型僅出現在規範中,而不出現在 JavaScript 語言中。JavaScript 引擎不需要(但可以選擇)實現它們。在這篇博客文章中,我們會認識到規範類型 Record(及其子類型 Completion Record)。
抽象操作
抽象操作 是在 ECMAScript 規範中定義的函數;它們的目的是為了簡潔地撰寫規範。JavaScript 引擎不需要將它們實現為引擎內部的單獨函數,它們無法直接從 JavaScript 調用。
內部槽與內部方法
內部槽 和 內部方法 使用以 [[ ]] 包圍的名稱。
內部槽是 JavaScript 對象或規範類型的數據成員,用於存儲對象的狀態。內部方法是 JavaScript 對象的成員函數。
例如,每個 JavaScript 對象都有一個內部槽 [[Prototype]] 和一個內部方法 [[GetOwnProperty]]。
內部槽和方法無法從 JavaScript 中訪問。例如,您不能訪問 o.[[Prototype]] 或調用 o.[[GetOwnProperty]]()。JavaScript 引擎可以自行實現它們,但並非必須這麼做。
有時內部方法會委託給類似名稱的抽象操作,例如普通對象的 [[GetOwnProperty]]:
當
O的[[GetOwnProperty]]內部方法以屬性鍵P被呼叫時,將按照以下步驟執行:
- 返回
! OrdinaryGetOwnProperty(O, P)。
(我們會在下一章節瞭解驚嘆號的含義。)
OrdinaryGetOwnProperty 不是一種內部方法,因為它並不與任何物件相關聯;相反,運作時的物件作為一個參數傳遞。
OrdinaryGetOwnProperty 被稱為“普通”,因為它針對普通物件操作。ECMAScript 中的物件可以是 普通的 或 特殊的。普通物件必須具有一組方法的預設行為,這些方法稱為 基本內部方法。如果物件偏離了預設行為,那它就是特殊的。
最知名的特殊物件是 Array,因為其長度特性以非預設的方式運作:設置 length 特性可能會移除 Array 中的元素。
基本內部方法的列表列於 這裡。
完成記錄
那麼,問題標誌和驚嘆號呢?為了瞭解它們,我們需要研究 完成記錄!
完成記錄是一種規範類型(僅為規範目的所定義)。JavaScript 引擎不必擁有對應的內部數據類型。
完成記錄是一種“記錄”——具有一組固定命名欄位的數據類型。完成記錄有三個欄位:
| 名稱 | 描述 |
|---|---|
[[Type]] | 其中之一:normal、break、continue、return 或 throw。除 normal 外,其他類型都是 突然完成。 |
[[Value]] | 完成時產生的值,例如函數的返回值或拋出的異常。 |
[[Target]] | 用於定向控制傳遞(與本文無關)。 |
每個抽象操作會隱式返回一個完成記錄。即使看起來像是抽象操作會返回一種簡單類型(例如布林值),它實際上隱式包裝在一個類型為 normal 的完成記錄中(見 隱式完成值)。
注意 1:規範在這方面並不完全一致;某些輔助函數返回純值,其返回值被直接使用而不從完成記錄中提取出來。這通常從上下文中可以清楚看出。
注意 2:規範的編輯者正在研究如何使完成記錄的處理更加明確。
如果算法拋出異常,這意味著返回一個完成記錄,其中 [[Type]] 為 throw,其 [[Value]] 為異常對象。我們暫時忽略 break、continue 和 return 類型。
ReturnIfAbrupt(argument) 的含義是執行以下步驟:
- 如果
argument是突然完成,則返回argument。- 將
argument設置為argument.[[Value]]。
也就是說,我們檢查一個完成記錄;如果它是突然完成,立即返回。否則,我們從完成記錄中提取值。
ReturnIfAbrupt 可能看起來像一個函數調用,但它不是。它使得包含 ReturnIfAbrupt() 的函數返回,而不是返回 ReturnIfAbrupt 函數本身。它更像是 C 語言類似語言中的宏。
ReturnIfAbrupt 可以像這樣使用:
- 讓
obj為Foo()。(obj是一個完成記錄。)ReturnIfAbrupt(obj)。Bar(obj)。(如果我們仍然在這裡,obj是從完成記錄中提取的值。)
現在,問題標誌 發揮作用:? Foo() 等同於 ReturnIfAbrupt(Foo())。使用速記是實用的:我們不需要每次顯式地編寫錯誤處理代碼。
類似地,Let val be ! Foo() 等同於:
- 讓
val為Foo()。- 斷言:
val不是突然完成。- 將
val設置為val.[[Value]]。
利用這些知識,我們可以如下重寫 Object.prototype.hasOwnProperty:
Object.prototype.hasOwnProperty(V)
- 令
P為ToPropertyKey(V)。- 如果
P是異常完成,返回P- 將
P設為P.[[Value]]- 令
O為ToObject(this value)。- 如果
O是異常完成,返回O- 將
O設為O.[[Value]]- 令
temp為HasOwnProperty(O, P)。- 如果
temp是異常完成,返回temp- 將
temp設為temp.[[Value]]- 返回
NormalCompletion(temp)
…然後我們可以這樣重寫 HasOwnProperty:
HasOwnProperty(O, P)
- 斷言:
Type(O)是Object。- 斷言:
IsPropertyKey(P)為true。- 令
desc為O.[[GetOwnProperty]](P)。- 如果
desc是異常完成,返回desc- 將
desc設為desc.[[Value]]- 如果
desc為undefined,返回NormalCompletion(false)。- 返回
NormalCompletion(true)。
我們也能在不使用驚嘆號的情況下重寫 [[GetOwnProperty]] 內部方法:
O.[[GetOwnProperty]]
- 令
temp為OrdinaryGetOwnProperty(O, P)。- 斷言:
temp不是異常完成。- 將
temp設為temp.[[Value]]。- 返回
NormalCompletion(temp)。
這裡假設 temp 是一個全新的臨時變量,並且不會與任何其他變量衝突。
我們還運用了這個知識:當返回語句返回的不是完成記錄時,會被隱式包裝在 NormalCompletion 中。
小補充:Return ? Foo()
規範使用了記法 Return ? Foo() — 為什麼加問號?
Return ? Foo() 展開為:
- 令
temp為Foo()。- 如果
temp是異常完成,返回temp。- 將
temp設為temp.[[Value]]。- 返回
NormalCompletion(temp)。
這與 Return Foo() 是相同的;對於異常和正常完成均以相同方式運行。
Return ? Foo() 僅用於編輯的原因,以便更明確地表達 Foo 返回的是一個完成記錄。
斷言
規範中的斷言用於斷言算法的不變條件。它們是為了增加清晰度,但對於實現並不增加任何要求 —— 實現不需要檢查它們。
前進
抽象操作委派給其他抽象操作(請參見下圖),但基於這篇博客,我們應當能弄清楚它們的作用。我們將遇到屬性描述符,它是另一種規範類型。
總結
我們讀了一個簡單的函數 —— Object.prototype.hasOwnProperty —— 以及它所調用的 抽象操作。我們熟悉了 ? 和 ! 的簡寫,它們與錯誤處理相關。我們還遇到了 語言類型,規範類型,內部插槽 和 內部方法。
有用的鏈接
如何閱讀 ECMAScript 規範:一個教程,涵蓋了這篇文章中的大量內容,並且視角略有不同。