跟着V8引擎读懂 ECMAScript 规范(三)原创
本篇,我们将深入阅读 ECMAScript 语言及其语法的定义。 如果您不熟悉上下文无关语法,可以从此 Wikipedia 快速了解一番,因为规范使用上下文无关语法来定义语言。
ECMAScript 语法
ECMAScript 规范定义了四种语法:
- 词法语法描述了如何将 Unicode 代码点转换为一系列输入元素(记号、行终止符、注释、空格)。
- 句法语法定义了句法正确的程序是如何由记号组成的。
- RegExp 语法描述了如何将 Unicode 代码点转换为正则表达式。
- 数字字符串语法描述了如何将字符串转换为数值。
每种语法都被定义为上下文无关语法,都是由一组产生式组成。
语法使用稍有不同的符号:句法语法使用 LeftHandSideSymbol : 而词法语法和 RegExp 语法使用 LeftHandSideSymbol :: 而数字字符串语法使用 LeftHandSideSymbol :::。
接下来我们将更详细地研究词汇语法和句法语法。
词法语法
该规范将 ECMAScript 源文本定义为一系列 Unicode 代码点。 例如,变量名称不限于 ASCII 字符,还可以包括其他 Unicode 字符。 该规范没有讨论实际的编码(例如,UTF-8 或 UTF-16)。 它假定源代码已根据其所在的编码转换为一系列 Unicode 代码点。
因为无法预先对 ECMAScript 源代码进行标记,这使得定义词法语法稍微复杂一些。
例如,如果不查看它出现的更大上下文,我们就无法确定 / 是除法运算符还是 RegExp 的开头:
const x = 10 / 5;
此处的 / 表示一个除法符号 DivPunctuator
const r = /foo/;
此处第一个 / 表示正则表达式字面量 RegularExpressionLiteral 的开始。
模板引入了类似的歧义 —— }` 的解释取决于它出现的上下文:
const what1 = 'temp';
const what2 = 'late';
const t = `I am a ${ what1 + what2 }`;
此处 `I am a ${ is TemplateHead and }` 表示一个模版结尾 TemplateTail
if (0 == 1) {
}`not very useful`;
此处 } 表示一个右大括号标点符号 RightBracePunctuator , 第一个 ` 表示无替代模板(纯字符串) NoSubstitutionTemplate 的开始。
尽管 / 和 }` 的解释取决于它们的“上下文”(它们在代码句法结构中的位置),我们接下来要描述的语法仍然是上下文无关的。
词法语法使用几个目标符号来区分允许某些输入元素和不允许某些输入元素的上下文。 例如,目标符号 InputElementDiv 用于 / 是除法,且 /= 是除法赋值的上下文中。 InputElementDiv 产生式列出了可以在此上下文中产生的可能标记:
InputElementDiv ::
WhiteSpace
LineTerminator
Comment
CommonToken
DivPunctuator
RightBracePunctuator
在这种情况下,遇到 / 会产生 DivPunctuator 输入元素。 在这里生成 RegularExpressionLiteral 并不是适合。
另一方面,InputElementRegExp 是上下文的目标符号,其中 / 是 RegExp 的开头:
InputElementRegExp ::
WhiteSpace
LineTerminator
Comment
CommonToken
RightBracePunctuator
RegularExpressionLiteral
正如我们从产生式中看到的,这可能会产生 RegularExpressionLiteral 输入元素,但产生 DivPunctuator 是不可能的。
类似地,除了 RegularExpressionLiteral 之外,还有另一个目标符号 InputElementRegExpOrTemplateTail,用于允许 TemplateMiddle 和 TemplateTail 的上下文。 最后, InputElementTemplateTail 是仅允许 TemplateMiddle 和 TemplateTail 而不允许 RegularExpressionLiteral 的上下文的目标符号。
在具体的实现中,句法语法分析器(“parser”)可以调用词法语法分析器(“tokenizer”或“lexer”),将目标符号作为参数传递并请求适合该目标符号的下一个输入元素。
句法语法
我们研究了词法语法,它定义了我们如何从 Unicode 代码点构造标记。 句法语法建立在它之上:它定义了句法正确的程序是如何由标记组成的。
示例:允许遗留标识符
在语法中引入一个新关键字可能是一个重大变化,如果现有代码已经使用该关键字作为标识符怎么办?
例如,在 await 是关键字之前,有人可能已经编写了以下代码:
function old() {
var await;
}
ECMAScript 语法小心地添加了 await 关键字,以使该代码继续工作。 在异步函数中,await 是一个关键字,所以这不起作用:
async function modern() {
var await; // Syntax error
}
在非生成器中允许 yield 作为标识符并在生成器中禁止它的工作方式类似。
了解如何允许 await 作为标识符需要了解 ECMAScript 的句法语法标记定义。
产生式和简写
让我们看看如何定义 VariableStatement 的产生式。 乍一看,语法看起来有点吓人:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
下标([Yield, Await])和前缀(+In 中的 + 和 ?Async 中的 ?)是什么意思?
下标是同时表达一组产生式、一组左侧符号的简写。 左侧符号有两个参数,扩展为四个“真实”左侧符号:VariableStatement、VariableStatement_Yield、VariableStatement_Await 和 VariableStatement_Yield_Await。
请注意,这里的普通 VariableStatement 表示“没有 _Await 和 _Yield 的变量语句”。 它不应与 VariableStatement[Yield, Await] 混淆。
在产生式的右侧,我们看到简写 +In,意思是“使用带有 _In 的版本”,以及 ?Await,意思是“当且仅当左侧符号有 _Await 时,使用带有 _Await 的版本” (与 ?Yield 类似)。
第三个简写,~Foo,意思是“使用没有 _Foo 的版本”,在这个产生式中没有使用。
有了这些信息,我们可以像这样扩展产生式:
VariableStatement :
var VariableDeclarationList_In ;
VariableStatement_Yield :
var VariableDeclarationList_In_Yield ;
VariableStatement_Await :
var VariableDeclarationList_In_Await ;
VariableStatement_Yield_Await :
var VariableDeclarationList_In_Yield_Await ;
最终,我们需要找出两件事:
- 1. 在哪里决定我们是处在有 _Await 还是没有 _Await 的情况中?
- 2. 它在哪里有所作为——Something_Await 和Something(没有_Await)的产生式在哪里区分?
有_Await 还是没有 _Await?
让我们先解决问题 1。 很容易猜到非异步函数和异步函数在我们是否为函数体选择参数 _Await 方面有所不同。 阅读异步函数声明的产生式,我们发现:
AsyncFunctionBody :
FunctionBody[~Yield, +Await]
请注意,AsyncFunctionBody 没有参数,它们被添加到右侧的 FunctionBody 中。
如果我们扩展这个产生式,会得到:
AsyncFunctionBody :
FunctionBody_Await
换句话说,异步函数具有 FunctionBody_Await,这意味着将 await 视为关键字的函数体。
另一方面,如果我们在一个非异步函数中,相关的产生式:
FunctionDeclaration[Yield, Await, Default] :
function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }
(FunctionDeclaration 有另一个产生式,但它与我们的代码示例无关。)
为避免组合扩展,我们忽略此产生式中未使用的 Default 参数。
产生式的扩展形式如下:
FunctionDeclaration :
function BindingIdentifier ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Yield :
function BindingIdentifier_Yield ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Await :
function BindingIdentifier_Await ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Yield_Await :
function BindingIdentifier_Yield_Await ( FormalParameters ) { FunctionBody }
在这个产生式中,我们总是得到 FunctionBody 和 FormalParameters(没有 _Yield 和没有 _Await),因为它们在非扩展产生式中使用 [~Yield, ~Await] 参数化。
函数名称的处理方式不同:如果左侧符号有出现 _Await 和 _Yield,则会获取它们作为参数。
总结一下:异步函数有一个 FunctionBody_Await,非异步函数有一个 FunctionBody(没有 _Await)。 由于我们讨论的是非生成器函数,因此我们的异步示例函数和非异步示例函数都在没有 _Yield 的情况下进行了参数化。
也许很难记住哪个是 FunctionBody 哪个是 FunctionBody_Await。 FunctionBody_Await 是用于 await 作为标识符的函数,还是用于 await 作为关键字的函数?
你可以认为 _Await 参数的意思是“await 是一个关键字”。 这种方法也是未来的证明。 想象一下,添加了一个新关键字 blob,但仅在“blobby”函数内部。 非 blobby 非异步非生成器仍然具有 FunctionBody(没有 _Await、_Yield 或 _Blob),就像现在一样。 Blobby 函数将具有 FunctionBody_Blob,异步 blobby 函数将具有 FunctionBody_Await_Blob 等等。 我们仍然需要为产生式添加 Blob 下标,但现有函数的 FunctionBody 的扩展形式保持不变。
不允许 await 作为标识符
接下来,如果我们在 FunctionBody_Await 中,我们需要了解如何禁止 await 作为标识符。
我们可以进一步跟踪产生式,看到 _Await 参数从 FunctionBody 一直到我们之前查看的 VariableStatement 产生式都保持不变。
因此,在异步函数内部,会有一个 VariableStatement_Await,而在非异步函数内部,会有一个 VariableStatement。
我们可以进一步跟踪产生式并跟踪参数。 我们已经看到了 VariableStatement 的产生式:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
VariableDeclarationList 的所有产生式都按原样携带参数:
VariableDeclarationList[In, Yield, Await] :
VariableDeclaration[?In, ?Yield, ?Await]
(这里我们只展示与我们的例子相关的产生式。)
VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt
opt 简写表示右边的符号是可选的; 实际上有两种产生式,一种带有可选符号,一种没有。
在与我们的示例相关的简单案例中,VariableStatement 由关键字 var 组成,后跟一个没有初始值设定项的 BindingIdentifier,并以分号结尾。
要禁止或允许 await 作为 BindingIdentifier,我们希望最终得到如下结果:
BindingIdentifier_Await :
Identifier
yield
BindingIdentifier :
Identifier
yield
await
这将不允许 await 作为异步函数内部的标识符,并允许它作为非异步函数内部的标识符。
规范中并没有那样去定义它,我们是从这个产生式中找到的:
BindingIdentifier[Yield, Await] :
Identifier
yield
await
扩展后可得到如下产生式:
BindingIdentifier_Await :
Identifier
yield
await
BindingIdentifier :
Identifier
yield
await
(我们省略了示例中不需要的 BindingIdentifier_Yield 和 BindingIdentifier_Yield_Await 的产生式。)
这看起来像 await 和 yield 总是被允许作为标识符。 那是怎么回事?
静态语义救援
事实证明,在异步函数中禁止将 await 作为标识符需要静态语义。
静态语义描述静态规则——即在程序运行之前检查的规则。
在这种情况下,BindingIdentifier 的静态语义定义了以下语法指导规则:
BindingIdentifier[Yield, Await] : await
如果此产生式具有 [Await] 参数,则为语法错误。
实际上,这禁止了 BindingIdentifier_Await :等待生产。
规范解释了有此产生式但通过静态语义将其定义为语法错误的原因是因为干扰了自动分号插入 (ASI)。
请记住,当我们无法根据语法产生式解析一行代码时,ASI 就会发挥作用。 ASI 尝试添加分号以满足语句和声明必须以分号结尾的要求。
考虑以下代码(来自规范的示例):
async function too_few_semicolons() {
let
await 0;
}
如果语法不允许 await 作为标识符,ASI 将启动并将代码转换为以下语法正确的代码,该代码也使用 let 作为标识符:
async function too_few_semicolons() {
let;
await 0;
}
这种对 ASI 的干扰被认为过于混乱,因此使用静态语义来禁止 await 作为标识符。
禁止 StringValues 标识符
还有另一个相关的规则:
BindingIdentifier : Identifier
如果此产生式具有 [Await] 参数并且 StringValue 标识符为“await”,则为语法错误。
起初这可能会令人困惑。 标识符定义如下:
Identifier :
IdentifierName but not ReservedWord
await 是保留字,那么标识符怎么可能是 await 的呢?
事实证明,Identifier 不能是 await,但它可以是 StringValue 为“await”的其他东西——字符序列 await 的不同表示。
标识符名称的静态语义定义了如何计算标识符名称的 StringValue。 例如,a 的 Unicode 转义序列是 \u0061,因此 \u0061wait 的 StringValue 为“await”。 \u0061wait 不会被词法语法识别为关键字,而是一个标识符。 禁止在异步函数中将其用作变量名的静态语义。
所以,以下代码可以运行:
function old() {
var \u0061wait;
}
以下代码则不行:
async function modern() {
var \u0061wait; // Syntax error
}
小结
在本文,我们熟悉了词法语法、句法语法以及用于定义句法语法的简写。 我们研究了在异步函数中禁止使用 await 作为标识符,但在非异步函数中它却可以使用的相关定义。