如何阅读 ECMAScript 规范 - 第三部分

·  阅读 77

[原文链接](Understanding the ECMAScript spec, part 3 · V8)

本文我们将深入了解 ECMAScript 语言及其语法的定义。如果你还不熟悉上下文无关的语法,现在最好去了解下基本知识,因为规范正是使用上下文无关的语法来定义语言的。请参阅 the chapter about context free grammars in "Crafting Interpreters" 获取简要的介绍或是通过 Wikipedia 获取更详细的数学定义。

ECMAScript语法(ECMAScript grammars)

ECMAScript 规范定义了四类语法:

词法(lexcial grammar) 描述如何将 Unicode 编码 转换为一系列输入元素(input elements)(标记、行终止符、注释、空格)

句法(syntactic grammar) 定义句法正确的程序是如何由标记(tokens)构成的

正则表达式语法(RegExp grammar) 描述如何将 Unicode 编码转换为正则表达式

数字字符串语法(numeric string grammar) 描述如何将字符串转换为数值

每个语法都定义为上下文无关的语法,由一组产品(production)组成。

不同语法使用的符号略有差别:句法使用 LeftHandSideSymbol :,词法和正则表达式语法则使用LeftHandSideSymbol ::,数字字符串语法使用LeftHandSideSymbol :::

接下来,我们会详细地研究词法和句法。

词法

规范将 ECMAScript 源代码定义为 Unicode 编码序列。例如,变量名不仅限于 ASCII 字符,还可以包括其它 Unicode 字符。规范并没有讨论实际的编码方式(例如 UTF-8 或 UTF-16)。它假设源代码已经根据它所使用的编码方式转换成了 Unicode 编码序列。

预先对 ECMAScript 源代码进行标记是不可能的,这使得定义词法会稍微复杂一些。

例如,我们不能确定 / 是除法运算符还是 RegExp 的开始,除非能看到更多的上下文信息。

const x = 10 / 5;
复制代码

这里 / 是除法运算符。

const r = /foo/;
复制代码

而这里则是正则字面量的开始。

模板也引入了类似的歧义 - 对 }' 的解释取决于它出现的上下文:

const what1 = 'temp';
const what2 = 'late';
const t = `I am a ${ what1 + what2 }`;
复制代码

这里 I am a ${ 是字面量的头,而 } 则是字面量的尾。

if (0 == 1) {
}`not very useful`;
复制代码

这里 } 是右花括号,` 是常量模板的头。

尽管对于 / 和 }` 的解释依赖其上下文 - 它们在代码语法结构中的位置 - 接下来我们描述的语法仍然是上下文无关的。

词法使用几个特定的符号(symbols)来区分是否允许输入元素(input elements)出现的上下文。例如,符号 InputElementDiv 会出现在带有 / 除法和 /= 除等于的上下文中。InputElementDiv 产品列出了在该上下文中可能出现的符号:

InputElementDiv ::
  WhiteSpace
  LineTerminator
  Comment
  CommonToken
  DivPunctuator
  RightBracePunctuator
复制代码

在这里的上下文中,遇到 / 会产生 DivPunctuator 输入元素。RegularExpressionLiteral 并不会出现上上面的选项中。

另一方面,InputElementRegExp 会出现在 / 作为 RegExp 开头的上下文中:

InputElementRegExp ::
  WhiteSpace
  LineTerminator
  Comment
  CommonToken
  RightBracePunctuator
  RegularExpressionLiteral
复制代码

正如我们所看到的,InputElementRegExp 可能会产生 RegularExpressionLiteral 输入元素,但不可能产生 Divpuncator。

类似地,还有另一个符号 InputElementRegExpOrTemplateTail,用于允许产生 TemplatMiddle 和TemplateTail 的上下文,除了 RegularExpressionLiteral。最后,InputElementTemplateTail 是只允许TemplatMiddle 和 TemplateTail 而不允许 RegularExpressionLiteral 产生的上下文的符号。

在具体实现中,句法分析器("parser")可以调用词法分析器("tokenizer" 或 "lexer"),将目标符号作为参数传递,并请求适合该目标符号的下一个输入元素。

句法

前面我们研究了词法。它定义了如何通过 Unicode 编码构造标记。句法建立在它的基础上:它定义了句法正确的程序如何由标记组成。

示例:允许原有标识符(Example: Allowing legacy identifiers)

在语法中引入一个新的关键字可能是一个破坏性的改变 - 如果现有的代码已经使用该关键字作为标识符怎么办?

例如,在 await 成为一个关键字之前,有人可能已经编写了如下代码:

function old() {
  var await;
}
复制代码

ECMAScript 语法谨慎地添加了 await 关键字,以使这段代码能够继续运行。在 async 函数内部,await 是一个关键字,所以下面的代码不能正常运行。

async function modern() {
  var await; // Syntax error
}
复制代码

在非生成器中允许使用 yield 和在生成器中不允许使用 yield 也是一样的道理。

要理解为何 await 可以作为标识符需要理解 ECMAscript 特定的句法符号。让我们开始吧!

产品和简写(Productions and shorthands)

让我们看看 VariableStatement 中的产品是如何定义的。咋一看,语法有点吓人。

VariableStatement[Yield, Await] :
  var VariableDeclarationList[+In, ?Yield, ?Await] ;
复制代码

这里的下标([Yield, Await])和前缀(+In 中的 + 以及 ?Async 中的 ?)是什么意思?

这些符号的解释在 Grammar Notation 章节。

下标是对于一组产品中所有左侧符号的简写。左侧符号有两个参数,由此扩展成实际存在的四个符号:VariableStatementVariableStatement_YieldVariableStatement_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. 哪里才会表现出这两者的差别

是否带 _Await

让我们先解决第一个问题。很容易猜测到,非异步函数和异步函数的区别在于我们是否为函数体添加了参数 _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 }
复制代码

展开后的组合全都包含了 FunctionBodyFormalParameters (不带 _Yield_Await),因为在未展开的产品中带有 [~Yield, ~Await] 参数。

函数名则表现得不同:它获得了左侧符号带有的 _Await 和 _Yield 参数。

总结一下:异步函数包含了 FunctionBody_Await,非异步函数则包含了 FunctionBody(不带 _Await)。因为我们讨论的是非生成器(non-generator)函数,异步函数示例和非异步函数示例都不带参数 _Yield

也许很难记住哪个是 FunctionBody,哪个是 FunctionBody_Await。FunctionBody_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[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 总是可以作为标识符。这是怎么回事?难道整篇文章都是白写的吗?

静态语义来拯救(Statics semantics to the rescue)

事实证明,在异步函数中禁止 await 作为标识符需要静态语义。

静态语义描述静态规则 - 也就是在程序运行之前要检查的规则。

对于本例,static semantics for BindingIdentifier 定义了如下语法规则:

BindingIdentifier[Yield, Await] : await
复制代码

It is a Syntax Error if this production has an [Await] parameter.

这实际上禁止了 BindingIdentifier_Await: await。

规范解释了通过静态语义将该产品定义为语法错误是因为其干扰了分号的自动插入(ASI)。

记住,当我们无法根据语法产品解析一行代码时,ASI 就会生效。ASI 会尝试添加分号来满足语句和声明必须以分号结尾的要求(我们会在后面的文章中详细探讨 ASI)。

思考下面的代码(选自规范):

async function too_few_semicolons() {
  let
  await 0;
}
复制代码

如果语法不允许使用 await 作为标识符,ASI 就会介入将代码转换成下面语法正确的代码,这里同样使用了 let 作为标识符。

async function too_few_semicolons() {
  let;
  await 0;
}
复制代码

这种 ASI 干扰被认为过于混乱。因此使用静态语义来禁止 await 作为标识符。

禁止使用标识符的字符串值(Disallowed StringValues of identifiers)

还有另外一个相关规则:

BindingIdentifier : Identifier
复制代码

It is a Syntax Error if this production has an [Await] parameter and StringValue of Identifier is "await".

咋一看会让人感到困惑,Identifier 的定义如下:

Identifier :
  IdentifierName but not ReservedWord
复制代码

await 是一个保留字,那么标识符怎么可能是 await 呢?

事实证明,Identifier 不能是 await,但它可以是字符串值为 "await" 的其他东西 - 字符序列 await 的不同表示形式。

Static semantics for identifier names 定义了如何计算标识符的字符串值。a 的 Unicode 转义序列是 \u0061,因此 \u0061wait 的字符串值就是 "await"。\u0061wait 不会被词法识别为关键字,相反它是标识符。静态语法禁止在异步函数中将其作为变量名。

因此,下面的代码是正确的:

function old() {
  var \u0061wait;
}
复制代码

而下面的则会报错:

async function modern() {
  var \u0061wait; // Syntax error
}
复制代码

总结

在本文中,我们了解了词法、句法以及用于定义词法的简写。通过一个示例,我们看到了异步函数中是不能使用 await 作为关键字的,而在非异步函数中是被允许的。

其它有趣的语法,如自动分号插入和 cover 语法,将在后面的文章中介绍。请继续关注!

分类:
前端
收藏成功!
已添加到「」, 点击更改