理解 ECMAScript 規範,第 4 部份
同時在網絡的其他部分
Jason Orendorff自 Mozilla 發佈了一篇深入分析 JS 語法怪癖的文章。即使實現細節不同,每個 JS 引擎都面臨著相同的這些怪癖問題。
掩覆語法
在本章節中,我們更深入地研究 掩覆語法。它們是一種用來指定看似模糊的語法構造的方法。
同樣,我們將省略 [In, Yield, Await]
的下標簡寫,此處對本博客文章並不重要。請參閱第 3 部份了解其含義和用法。
有限前瞻
通常,解析器根據有限的前瞻(固定數量的後續詞)來決定使用哪個生成規範。
在某些情況下,下一個詞可以明確地決定要使用哪個生成規範。例如:
UpdateExpression :
LeftHandSideExpression
LeftHandSideExpression ++
LeftHandSideExpression --
++ UnaryExpression
-- UnaryExpression
如果我們正在解析 UpdateExpression
,且下一個詞是 ++
或 --
,我們可以立即知道要使用的生成規範。如果下一個詞既不是 ++
也不是 --
,情況仍然不會太糟糕:我們可以從當前位置開始解析一個 LeftHandSideExpression
,解析後再決定下一步的操作。
如果緊接在 LeftHandSideExpression
之後的詞是 ++
,則使用的生成規範為 UpdateExpression : LeftHandSideExpression ++
。--
的情況類似。而如果緊接在 LeftHandSideExpression
之後的詞既不是 ++
,也不是 --
,我們則使用生成規範 UpdateExpression : LeftHandSideExpression
。
箭頭函數參數列表還是括號表達式?
區分箭頭函數參數列表和括號表達式則更加復雜。
例如:
let x = (a,
這是像這樣的箭頭函數的開頭嗎?
let x = (a, b) => { return a + b };
或者可能是像這樣的括號表達式?
let x = (a, 3);
括號中的內容可以任意長——我們對其內容的判斷不能依賴有限數量的詞。
假設我們有以下簡單的生成規範:
AssignmentExpression :
...
ArrowFunction
ParenthesizedExpression
ArrowFunction :
ArrowParameterList => ConciseBody
現在我們無法通過有限的前瞻選擇合適的生成規範。如果我們需要解析一個 AssignmentExpression
,而下一個詞是 (
,如何決定解析哪個生成規範呢?我們既可以解析 ArrowParameterList
,也可以解析 ParenthesizedExpression
,但我們的猜測可能是錯的。
非常寬鬆的新符號:CPEAAPL
規範通過引入符號 CoverParenthesizedExpressionAndArrowParameterList
(簡稱為 CPEAAPL
)來解決此問題。CPEAAPL
實際上是一個 ParenthesizedExpression
或 ArrowParameterList
的符號,但我們目前還不知道是哪一個。
生成規範對 CPEAAPL
非常寬鬆,允許 ParenthesizedExpression
和 ArrowParameterList
中出現的所有構造:
CPEAAPL :
( Expression )
( Expression , )
( )
( ... BindingIdentifier )
( ... BindingPattern )
( Expression , ... BindingIdentifier )
( Expression , ... BindingPattern )
例如,以下表達式都是有效的 CPEAAPL
:
// 有效的 ParenthesizedExpression 和 ArrowParameterList:
(a, b)
(a, b = 1)
// 有效的 ParenthesizedExpression:
(1, 2, 3)
(function foo() { })
// 有效的 ArrowParameterList:
()
(a, b,)
(a, ...b)
(a = 1, ...b)
// 非有效的表達式,但仍屬於 CPEAAPL:
(1, ...b)
(1, )
尾隨逗號和 ...
只能在 ArrowParameterList
中出現。一些構造,例如 b = 1
可以出現在兩者中,但它們的含義不同:在 ParenthesizedExpression
中,它是賦值,在 ArrowParameterList
中,它是帶有默認值的參數。數字和其他 PrimaryExpressions
(無效的參數名稱或參數解構模式)只能出現在 ParenthesizedExpression
中。但它們都可以出現在 CPEAAPL
中。
在生成規範中使用 CPEAAPL
現在我們可以在 AssignmentExpression
的產物 中使用非常寬鬆的 CPEAAPL
。(注意:ConditionalExpression
通過一長串的產物鏈引向 PrimaryExpression
,這裡未顯示)。
AssignmentExpression :
ConditionalExpression
ArrowFunction
...
ArrowFunction :
ArrowParameters => ConciseBody
ArrowParameters :
BindingIdentifier
CPEAAPL
PrimaryExpression :
...
CPEAAPL
想像我們再次處於需要解析 AssignmentExpression
且下一個符號是 (
的情境。現在我們可以解析 CPEAAPL
並稍後再決定使用哪一個產物。無論我們是在解析 ArrowFunction
還是 ConditionalExpression
,下一個需要解析的符號都是 CPEAAPL
!
在解析完 CPEAAPL
之後,我們可以根據 CPEAAPL
後面的符號來決定使用原始的 AssignmentExpression
(包含 CPEAAPL
的那個)的哪一個產物。
如果符號是 =>
,我們使用以下產物:
AssignmentExpression :
ArrowFunction
如果符號是其他的東西,我們使用以下產物:
AssignmentExpression :
ConditionalExpression
例如:
let x = (a, b) => { return a + b; };
// ^^^^^^
// CPEAAPL
// ^^
// CPEAAPL 後面的符號
let x = (a, 3);
// ^^^^^^
// CPEAAPL
// ^
// CPEAAPL 後面的符號
在這個時候我們可以保留 CPEAAPL
原樣並繼續解析程式的其餘部分。例如,如果 CPEAAPL
位於 ArrowFunction
內,我們暫時不需要檢查它是否是一個有效的箭頭函數參數列表——這可以稍後完成。(實際上的解析器可能選擇立即進行有效性檢查,但從規範的角度來看,我們不需要這樣做。)
限制 CPEAAPL
如之前所述,CPEAAPL
的文法產物非常寬鬆,允許一些永遠不合法的構造(例如 (1, ...a)
)。在根據文法解析程式之後,我們需要禁止對應的不合法構造。
規範通過添加以下限制來實現此目的:
PrimaryExpression : CPEAAPL
如果
CPEAAPL
沒有覆蓋一個ParenthesizedExpression
則語法錯誤。
在處理以下產物的實例時
PrimaryExpression : CPEAAPL
使用以下文法來細化對
CPEAAPL
的解釋:
ParenthesizedExpression : ( Expression )
這表示:如果在語法樹的 PrimaryExpression
位置出現 CPEAAPL
,則實際上它是一個 ParenthesizedExpression
,並且這是唯一有效的產物。
Expression
永遠不可以是空的,因此 ( )
不是有效的 ParenthesizedExpression
。透過 逗號操作符 可以創建用逗號分隔的列表,例如 (1, 2, 3)
:
Expression :
AssignmentExpression
Expression , AssignmentExpression
類似地,如果 CPEAAPL
出現在 ArrowParameters
的位置,則適用以下限制:
ArrowParameters : CPEAAPL
如果
CPEAAPL
沒有覆蓋一個ArrowFormalParameters
則語法錯誤。
當產物
ArrowParameters
:CPEAAPL
被識別時,使用以下文法來細化對
CPEAAPL
的解釋:
ArrowFormalParameters :
( UniqueFormalParameters )
其他覆蓋文法
除了 CPEAAPL
,規範還對其他看似模糊的構造使用覆蓋文法。
ObjectLiteral
被用作 ObjectAssignmentPattern
的覆蓋文法,後者出現在箭頭函數參數列表內。這意味著 ObjectLiteral
允許一些不能出現在實際物件字面值中的構造。
ObjectLiteral :
...
{ PropertyDefinitionList }
PropertyDefinition :
...
CoverInitializedName
CoverInitializedName :
IdentifierReference Initializer
Initializer :
= AssignmentExpression
例如:
let o = { a = 1 }; // 語法錯誤
// 帶有預設值的解構賦值參數的箭頭函數:
//
let f = ({ a = 1 }) => { return a; };
f({}); // 返回 1
f({a : 6}); // 返回 6
有限前瞻下,非同步箭頭函數看起來也會模糊不清:
let x = async(a,
這是在呼叫名為 async
的函數還是非同步箭頭函數?
let x1 = async(a, b);
let x2 = async();
function async() { }
let x3 = async(a, b) => {};
let x4 = async();
為此,文法定義了一個類似於 CPEAAPL
的覆蓋文法符號 CoverCallExpressionAndAsyncArrowHead
。
摘要
在本集節目中,我們深入探討了規範如何定義覆蓋語法,並在無法基於有限前瞻確定當前語法結構的情況下使用它們。
尤其是,我們研究了如何區分箭頭函數參數列表與括號表達式,以及規範如何使用覆蓋語法先寬鬆地解析模糊的結構,然後用靜態語義規則進一步限制它們。