[原文链接](Understanding the ECMAScript spec, part 4 · V8)
同时存在于 Web 中的其它部分(Meanwhile in other parts of the Web)
来自 Mozilla 的 Jason Orendorff 发表了一篇文章 对 JS 语法怪异行为的深入分析。尽管实现细节不同,但每个 JS 引擎都面临着这些怪异行为。
替代语法(Cover grammars)
本中,我们将深入了解替代语法。它们为最初看起来不明确的句法结构指定语法。
简便起见,我们会忽略在文中不太重要的下标 [In, Yield, Await],具体含义和用法请参考第三部分的内容 - 如何阅读 ECMAScript 规范 - 第三部分 - 掘金 (juejin.cn)。
有限超前(Finite lookaheads)
通常,解析器会适当向前看(固定数量的标记)来决定使用哪个产品。
在某些情况下,下一个标记会明确地决定要使用的产品。例如:
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 或parenthesizeddexpression,但我们的猜测可能会是错的。
非常灵活的新符号:CPEAAPL
规范通过引入符号 CoverParenthesizedExpressionAndArrowParameterList(简称 CPEAAPL)解决了这个问题。CPEAAPL 是一个符号,在幕后它实际是 ParenthesizedExpression 或 ArrowParameterList,但我们还不知道到底是哪一个。
CPEAAPL 的产品非常宽松,允许可以出现在 ParenthesizedExpressions 和 ArrowParameterLists 中的所有结构:
CPEAAPL :
( Expression )
( Expression , )
( )
( ... BindingIdentifier )
( ... BindingPattern )
( Expression , ... BindingIdentifier )
( Expression , ... BindingPattern )
例如,下面的表达式就是合法的 CPEAAPLs:
// Valid ParenthesizedExpression and ArrowParameterList:
(a, b)
(a, b = 1)
// Valid ParenthesizedExpression:
(1, 2, 3)
(function foo() { })
// Valid ArrowParameterList:
()
(a, b,)
(a, ...b)
(a = 1, ...b)
// Not valid either, but still a CPEAAPL:
(1, ...b)
(1, )
, ... 只能出现在 ArrowParameterList 中。像 b = 1 这样的结构既可以出现在 ArrowParameterList 中也可以出现在 ParenthesizedExpression 中,但含义却不同:在前者它表示一个带默认值的参数,在后者它表示一个赋值语句。数字和其它非有效参数名(或参数解构模式)的 PrimaryExpression 只能出现在ParenthesizedExpression 中。但是,所有这些都可以出现在 CPEAAPL 中。
在产品中使用 CPEAAPL
现在我们可以在 AssignmentExpression 的结果中使用非常宽松的 CPEAAPL。(注意:ConditionalExpression 会通过一个很长的产品链导向 PrimaryExpression,这里没有显示。)
AssignmentExpression :
ConditionalExpression
ArrowFunction
...
ArrowFunction :
ArrowParameters => ConciseBody
ArrowParameters :
BindingIdentifier
CPEAAPL
PrimaryExpression :
...
CPEAAPL
假设我们再次处于这样的情况中,我们需要解析 AssignmentExpression,并而下一个标记是 ( 。现在,我们可以解析 CPEAAPL,并在之后再确定要使用什么产品。不管我们解析的是 ArrowFunction 还是ConditionalExpression,下一个要解析的符号都是 CPEAAPL !
在解析了 CPEAAPL 之后,我们可以决定将哪个产品用于初始的 AssignmentExpression(包含 CPEAAPL 的那个)。这是依据 CPEAAPL 之后的标记所决定的。
如果该标记是 =>,我们使用下面的产品:
AssignmentExpression :
ArrowFunction
如果是其它的标记,我们使用下面的产品:
AssignmentExpression :
ConditionalExpression
例如:
let x = (a, b) => { return a + b; };
// ^^^^^^
// CPEAAPL
// ^^
// The token following the CPEAAPL
let x = (a, 3);
// ^^^^^^
// CPEAAPL
// ^
// The token following the CPEAAPL
此时,我们可以保持 CPEAAPL 不变,并继续解析程序的其余部分。例如,如果 CPEAAPL 在箭头函数内,我们还不需要检查它是否是一个有效的箭头函数参数列表 - 这可以稍后再做决定。(实际的解析器可能会选择立即进行有效性检查,但从规范的角度看,我们不需要这样做。)
限制 CPEAAPLs
正如我们在前面看到的,CPEAAPL 的语法结果是非常宽松的,并允许始终无效的结构(比如(1, ...a))。在根据语法解析完程序之后,我们需要禁止相应的非法结构。
规范通过添加下面的限制来做到这一点:
Static Semantics: Early Errors
PrimaryExpression : CPEAAPLIt is a Syntax Error if
CPEAAPLis not covering aParenthesizedExpression.
When processing an instance of the production
PrimaryExpression : CPEAAPLthe interpretation of the
CPEAAPLis refined using the following grammar:
ParenthesizedExpression : ( Expression )
这意味着:如果 CPEAAPL 出现在语法树中 PrimaryExpression 的位置,它实际上是一个 ParenthesizedExpression,这是它唯一有效的结果。
Expression 永远不可能为空,所以 () 不是一个有效的 ParenthesizedExpression。像 (1,2,3) 这样用逗号分隔的列表是由 逗号操作符 创建的:
Expression :
AssignmentExpression
Expression , AssignmentExpression
类似地,如果 CPEAAPL 出现在 ArrowParameters 的位置,则适用以下限制:
Static Semantics: Early Errors
ArrowParameters : CPEAAPLIt is a Syntax Error if
CPEAAPLis not covering anArrowFormalParameters.
When the production
ArrowParameters:CPEAAPLis recognized the following grammar is used to refine the interpretation of
CPEAAPL:
ArrowFormalParameters :
( UniqueFormalParameters )
其它替代语法
除了 CPEAAPL 之外,规范还为其他看起来模棱两可的结构使用了替代语法。
ObjectLiteral 被用作出现在箭头函数参数列表中的 ObjectAssignmentPattern 的覆盖语法。这意味着 ObjectLiteral 允许在实际对象字面量中不可能出现的构造。
ObjectLiteral :
...
{ PropertyDefinitionList }
PropertyDefinition :
...
CoverInitializedName
CoverInitializedName :
IdentifierReference Initializer
Initializer :
= AssignmentExpression
例如:
let o = { a = 1 }; // syntax error
// Arrow function with a destructuring parameter with a default
// value:
let f = ({ a = 1 }) => { return a; };
f({}); // returns 1
f({a : 6}); // returns 6
异步箭头函数在有限超前时看起来也很模糊:
let x = async(a,
这是异步函数的调用还是异步箭头函数的调用?
let x1 = async(a, b);
let x2 = async();
function async() { }
let x3 = async(a, b) => {};
let x4 = async();
为此,定义了一个替代语法符号 CoverCallExpressionAndAsyncArrowHead,它的工作方式与 CPEAAPL 类似。
总结
本文,我们研究了规范如何定义替代语法,并在基于有限超前无法识别当前句法结构的情况下如何使用替代语法。
特别是,我们研究了箭头函数参数列表与圆括号表达式的区别,以及规范如何使用替代语法首先允许解析看起来模棱两可的结构,然后用静态语义规则来限制它们。