翻译本文的目的是尝试给出 ECMAScript 规范中核心术语的译法,供同好品评。
这一次我们深入 ECMAScript 语言及其语法的定义。如果你不太熟悉上下文无关文法[1],应该先补补课,至少先弄懂一些基本概念。因为规范中使用了上下文无关文法定义语言。
ECMAScript 规范定义了 4 种文法。
每种文法都用上下文无关文法来定义,都包含一组产生式。
不同的文法使用了不同的表示方式。语法文法表示为LeftHandSideSymbol :
,词法文法和正则文法表示为LeftHandSideSymbol ::
,而数值字符串文法表示为LeftHandSideSymbol :::
。(以冒号的多少来区分。——译者注)
接下来我们详细分析一下词法文法和语法文法。
规范将 ECMAScript 源文本定义为一个 Unicode 码点序列。这意味着变量名并不限于 ASCII 字符,也可以包含其他 Unicode 字符。规范并没有谈到实际的编码(如 UTF-8 或 UTF-16),而是假设源代码已经按照自己的编码转换成了 Unicode 码点序列。
无法提前对 ECMAScript 源码进行标记化(tokenize),这使得定义词法文法略显复杂。比如,如果不看它所处的更大的上下文,就无法确定/是除法操作符还是正则表达式的开始。
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 ${
是TemplateHead
,而`}``是TemplateTail。
if (0 == 1) {
}`not very useful`;
这里}
是RightBracePunctuator
,而``是
NoSubstitutionTemplate`的开头。
即便对/
和}
的解释取决于上下文(它们在代码语法结构中的位置),我们下面介绍的文法仍然是上下文无关的。
词汇文法使用一些目标符号(goal symbol)来区分哪些上下文允许哪些输入元素,不允许哪些输入元素。例如,目标符号InputElementDiv
(注意,这里的Div
是 Divide,即除法的意思。——译者注)会用在/
是除法和/=
是除法赋值的上下文中。InputElementDiv
产生式列出了在此上下文中可能产生的标记:
InputElementDiv ::
WhiteSpace
LineTerminator
Comment
CommonToken
DivPunctuator
RightBracePunctuator
在这个上下文中,遇到/
产生输入元素DivPunctuator
,而不会产生RegularExpressionLiteral
。
相应地,对于/
是正则表达式开头的上下文,目标符号是InputElementRegExp
:
InputElementRegExp ::
WhiteSpace
LineTerminator
Comment
CommonToken
RightBracePunctuator
RegularExpressionLiteral
这个产生式可以产生RegularExpressionLiteral
输入元素,但不可能产生DivPunctuator
。
类似地,目标符号InputElementRegExpOrTemplateTail
对应的上下文除了RegularExpressionLiteral
,还允许出现TemplateMiddle
和TemplateTail
。最后一个InputElementTemplateTail
目标符号的上下文只允许TemplateMiddle
和TemplateTail
,不允许出现RegularExpressionLiteral
。
在实现中,语法文法分析器(“解析器”)可以调用词法文法分析器(“标记器”或“词法器”),将目标符号作为参数传递,并请求适合该目标符号的下一个输入元素。
词法文法定义了如何从 Unicode 码点构建标记。语法文法建立在它的基础上,定义了标记如何组成语法正确的程序。
给文法增加新关键字有可能造成破坏:如果原有代码已经使用该关键字作为标识符了怎么办?
例如,在await
还不是关键字的时候,可能出现这样的代码:
function old() {
var await;
}
ECMAScript 文法谨慎地添加了 await 关键字,以便这段代码可以继续工作。在 async 函数里面,await 是一个关键字,所以不能这样写:
async function modern() {
var await; // 语法错误
}
在非生成器中允许yield
,而在生成器中不允许与此类似。
要理解怎么允许await
作为标识符,需要理解 ECMAScript 特定的语法文法表示。
来看看VariableStatement
的产生式是怎么定义的。乍一看,这个文法有点吓人:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
这里的下标([Yield, Await]
)和前缀(+In
里的+
和?Await
里的?
)都什么意思呀?
这种表示法在“文法表示法[7]”中有解释。
下标是对一组产生式的简写形式,一次性表达了一组产生式左端(left-hand side)符号。这个产生式左端符号有两个参数,它们可以展开为四个“真正的”产生式左端符号:
VariableStatement
VariableStatement_Yield
VariableStatement_Await
VariableStatement_Yield_Await
注意,以上VariableStatement
就表示VariableStatement
,没有_Await
也没有_Yield
。不要把它跟<em>VariableStatement</em><sub style="line-height: 0;">[Yield, Await]</sub>
(简写形式)弄混了。
在产生式右端,可以看到简写+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 ;
最后,还有要搞清楚两件事。
_Await
或没有_Await
的情况下?Something_Await
的产生式和Something
(没有_Await
)的产生式是在哪里分叉的?_Await
还是没有_Await
先解决第一个问题。因为很容易猜到可以根据函数体是否带_Await
来区分异步函数和非异步函数。看异步函数声明的产生式,我们发现了这个[8]:
AsyncFunctionBody :
FunctionBody[~Yield, +Await]
注意AsyncFunctionBody
没有参数,参数在右端被添加给了FunctionBody
。展开这个产生式得到:
AsyncFunctionBody :
FunctionBody_Await
换句话说,异步函数有FunctionBody_Await
(即一个await
会被当成关键字的)函数体。
另一方面,如果是在非异步函数中,相关产生式[9]为:
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]
。
函数名的处理方式就不一样了:如果产生式左端符号中包含_Yield
和_Await
,则右端的函数名也会带上相应参数。
总结:异步函数有FunctionBody_Await
,而非异步函数有FunctionBody
(不带_Await
)。因为我们讨论的是非生成器函数,所以无论异步示例函数还是非异步示例函数,都不会带参数_Yield
。
可能记住哪个是FunctionBody
,哪个是FunctionBody_Await
有点难。在有FunctionBody_Await
的函数体中,await
是标识符,还是关键字?
可以把_Await
参数的意思理解为“await
是一个关键字。”这样理解在将来也不会有问题。假设将来又添加了一个blob
关键字,但只适用于“斑驳”(blobby)函数。非斑驳、非异步、非生成器的函数仍然有FunctionBody
(不带_Yield
、_Await
或_Blob
),跟现在完全一样。斑驳函数则会有FunctionBody_Await_Blob
等。虽然仍然要在产生式中添加Blob
下标,但已存在函数的FunctionBody
的展开形式还跟以前一样。
await
用作标识符接下来,我们要搞清楚在FunctionBody_Await
中,是怎么不允许await
用作标识符的。
仔细看一看产生式,可以发现从FunctionBody
到之前的VariableStatement
产生式都带_Await
参数。因此,在异步函数中,就有VariableStatement_Await
,而在非异步函数中,则有VariableStatement
。
再看仔细一点,注意一下参数。我们已经看到了这个VariableStatement[10]的产生式:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
所有VariableDeclarationList[11]的产生式也都照样带这些参数:
VariableDeclarationList[In, Yield, Await] :
VariableDeclaration[?In, ?Yield, ?Await]
(这里只展示与我们例子相关的产生式[12]。)
VariableDeclarationList[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer [?In, ?Yield, ?Await]opt ;
这里的opt
简写代表产生式右端符号是可选的,也就是实际上有两个产生式:一个带可选的符号(Initializer
),另一个不带。
对我们简单的例子来说,VariableStatement
包含关键字var
,紧跟一个BindingIdentifier
(没有初始化器Initializer
),以分号结束。
为了允许或不允许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的静态语义[13]定义了以下语法导向的规则:
BindingIdentifier[Yield, Await] : await
[Await]
参数就是一个语法错误(Syntax Error)。实际上,这就是禁止BindingIdentifier_Await : await
产生式。
规范解释说,之所以存在这个产生式但又通过静态语义将它定义为语法错误,是因为与 ASI(Automatic Semicolon Insertion,自动插入分号)冲突。
我们知道,在无法根据语法产生式解析一行代码时,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]
参数,且Identifier
的StringValue
是"await"
,就是一个语法错误(Syntax Error)。乍一看不好理解。Identifier 的定义[14]如下:
Identifier :
IdentifierName but not ReservedWord
await
是个ReservedWord
,因此Identifier
怎么可能是await
呢?
的确,Identifier
不可能是await
,但可以是StringValue
为"await"
(字符序列await
的一种不同的表示方式)的其他值。
标识符名的静态语义[15] 定义了如何计算标识符的StringValue
。比如,a
的 Unicode 转义序列是\u0061
,因此\u0061wait
的StringValue
是"await"
。\u0061wait
不会被词法语法识别为关键字,而会被识别为Identifier
。静态语义禁止在异步函数中使用它作为变量名。
因此,这样可以:
function old() {
var \u0061wait;
}
但这样不行:
async function modern() {
var \u0061wait; // 语法错误
}
通过学习这篇文章,我们了解了词法文法、语法文法,以及用于定义语法文法的简写形式。作为例子,我们研究了await
在异步函数中被禁止用作标识符,但在非异步函数中则允许。
下一篇文章将介绍词法文法其他有意思的部分,比如自动插入分号(ASI)和包含文法(cover grammar)。
[1] 文无关文法: https://en.wikipedia.org/wiki/Context-free_grammar
[2] 词法文法: https://tc39.es/ecma262/#sec-ecmascript-language-lexical-grammar
[3] Unicode码点: https://en.wikipedia.org/wiki/Unicode#Architecture_and_terminology
[4] 语法文法: https://tc39.es/ecma262/#sec-syntactic-grammar
[5] 正则文法: https://tc39.es/ecma262/#sec-patterns
[6] 数值字符串文法: https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type
[7] 文法表示法: https://tc39.es/ecma262/#sec-grammar-notation
[8] 这个: https://tc39.es/ecma262/#prod-AsyncFunctionBody
[9] 相关产生式: https://tc39.es/ecma262/#prod-FunctionDeclaration
[10] VariableStatement
: https://tc39.es/ecma262/#prod-VariableStatement
[11] VariableDeclarationList
: https://tc39.es/ecma262/#prod-VariableDeclarationList
[12] 产生式: https://tc39.es/ecma262/#prod-VariableDeclaration
[13] BindingIdentifier
的静态语义: https://tc39.es/ecma262/#sec-identifiers-static-semantics-early-errors
[14] Identifier的定义: https://tc39.es/ecma262/#prod-Identifier
[15] 标识符名的静态语义: https://tc39.es/ecma262/#sec-identifier-names-static-semantics-stringvalue
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8