likes
comments
collection
share

理解 ECMAScript 规范(三)

作者站长头像
站长
· 阅读数 19

发布于 2020 年 4 月 1 日,标签 ECMAScript 理解 ECMAScript

全集

在这节内容中,我们将会深入理解 ECMAScript 语言以及文法定义。ECMAScript 规范使用 上下文无关文法 定义语言,如果你这个文法还不了解的话,最好现在去了解一下它的基础文法知识。

ECMAScript 文法 #

ECMAScript 规范定义了四种文法:

词法文法描述了如何将 Unicode 码点 编译为输入元素(标记、换行符、注释、空格)序列。

语法文法定义了是如何用标记构成文法正确的程序。

正则文法描述了是如何将 Unicode 码点编译成正则表达式。

数值字符串文法描述了如何将字符串编译转译成数字类型的值。

每一种文法都由上下文无关文法进行定义,其中包含了一组产生式。

四种文法使用的表达式相差无二:语法文法表示为 LeftHandSideSymbol,词法文法和正则文法表示为 LeftHandSideSymbol ::,而数值字符串文法表示为 LeftHandSideSymbol :::

接下来,我们详细分析一下词法文法和语法文法。

词法文法 #

规范将 ECMAScript 源文本定义为一串 Unicode 码点序列。举个例子,变量名称不仅可以是 ASCII 字符,还可以包含其它的 Unicode 字符。规范并没有谈论实际的编码 (比如 UTF-8 或者 UTF-16)。规范已经假定源码已经按照自己的编码方式转化为了 Unicode 码点序列。

很难在初始时对 ECMAScript 源码进行标记,这会让定义词法文法会变得稍显复杂。

比如说,如果不去看 所在的上下文环境是什么,就不知道 指的是除法操作符还是正则表达式开头。

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 的开头。

即使 /}` 的解释取决于它们所在的上下文(它们在代码中的语法位置),但是我们接下来谈论的文法仍然是和上下文无关的。

词法文法中使用一些目标符来区分对不同上下文环境中允许哪些输入元素、不允许哪些输入元素。例如,目标符 InputElementDiv 被用于 / 是除法以及 /= 是除法赋值的上下文中。InputElementDiv 产生式列出了在次上下文中可能产生的标记:

InputElementDiv ::  WhiteSpace  LineTerminator  Comment  CommonToken  DivPunctuator  RightBracePunctuator

在这个上下文中,遇到 / 会产生 DivPunctuator ,并不会产生 RegularExpressionLiteral

同样地,在一个 / 是正则开头的上下文中,InputElementRegExp 是对应的目标符。

InputElementRegExp ::  WhiteSpace  LineTerminator  Comment  CommonToken  RightBracePunctuator  RegularExpressionLiteral

从上面的产生式中可以看出,可能产生 RegularExpressionLiteral 输入元素,但是不会产生 DivPunctuator

相似的,这里还有另一种目标符, 在目标符 InputElementRegExpOrTemplateTail 所对应的上下文环境中, 除了允许 RegularExpressionLiteral,还允许 TemplateMiddleTemplateTail。最后,InputElementTemplateTail 目标符对应的上下文环境只允许 TemplateMiddle and TemplateTail,不允许RegularExpressionLiteral 出现。

在实现过程中,语法文法分析器(解析器),又称为词法文法分析器(标记器或词法器),将目标符作为参数进行传递,并请求下一个符合这个目标符的输入元素。

Syntactic grammar # 语法文法 #

词法文法法定义了如何从 Unicode 码点构建标记。而语法文法在词法文法的基础上,定义了如何将标记组成正确的程序。

例子:允许合法的标志符 #

将一种新的关键词引入到文法中可能会造成翻天覆地的变化,要是原有的代码已经将这个关键词作为一个标志符在使用呢?

比如,在 await 被定为关键字之前,有人可能写过下面的代码:

function old() {  var await;}

ECMAScript 文法在不影响代码正常运行下小心地加入了 await 关键字。在异步函数中,await 是一个关键词,因此下面的代码不能正常运行:

async function modern() {  var await; // Syntax error}

类似地,在非生成器代码中 yield 可以是标志符,而在执行器中禁止使用作为标志符。

要理解 await 是如何被允许用作标志符的,需要理解 ECMAScript-specific 语法文法标记。让我们来看一下吧。

产生式和缩写 #

让我们来看看 VariableStatement 产生式是如何被定义的。乍一看时,下面的文法可能有点可怕。

VariableStatement[Yield, Await] :  var VariableDeclarationList[+In, ?Yield, ?Await] ;

下标([Yield, Await])和前缀(+In 里面的 +,以及 ?Async 里的 ?)表示什么意思呢?

这种表示法在文法表示法中有解释。

下标是表示一组产生式的缩写,总的来说,是一组产生式左端符号。产生式左端符号有两个参数,可以展开为四个”真正的“产生式左端符号,分别是:VariableStatement, VariableStatement_YieldVariableStatement_Await, 以及 VariableStatement_Yield_Await.

注意这里的 VariableStatement 只是表示 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的情况下?
  2. Something_AwaitSomething(没有 _Await) 产生式的差异在哪里?

_Await 还是没有 _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)。因为我们谈论的是非生成器函数,因此我们的异步函数例子和非异步函数都不携带 _Yield 参数。

要记住哪个是 FunctionBodyFunctionBody_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 被当作标记符的。

我们可以通过深入探究产生式,发现从 FunctionBody 到我们先前谈论的VariableStatement 产生式都携带了 _Await 参数。

因此,在一个异步函数中,有 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  yieldBindingIdentifier :  Identifier  yield  await

这表示不允许在异步函数中把 await 作为标志符,但是在非异步函数中允许作为标志符。

规范中并没有给出这样的定义,但是我们找到了这个产生式:

BindingIdentifier[Yield, Await] :  Identifier  yield  await

将其扩展,会得到下面的产生式:

BindingIdentifier_Await :  Identifier  yield  awaitBindingIdentifier :  Identifier  yield  await

(这里省略了例子里不需要的 BindingIdentifier_YieldBindingIdentifier_Yield_Await 产生式。)

看起来似乎 awaityield 在任何场景下都可以当作标志符。为什么呢?是不是这篇文章白费功夫?

静态语义来拯救

事实证明,禁止在异步函数中将 await 作为一个标识符使用, 需要用到静态语义

静态语义描述了一些在程序运行之前进行校验的静态规则。

在这里,BindingIdentifier 的静态语义 定义了以下语法文法导向的规则:

BindingIdentifier[Yield, Await] : await

如果这个产生式有一个 [Await] 参数,就是一个语法错误.

事实上,这会禁止 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 当成标识符。

不允许标识符的 StringValues #

这里还有另外一条相关的规则:

BindingIdentifier : Identifier

如果产生式有 [Await] 参数并且 IdentifierStringValue"await", 就是一个语法错误。

开始可能让人有点困惑。标识符 的定义是这样的:

Identifier :  IdentifierName but not ReservedWord

await 是一个 ReservedWord,所以 await 怎么可以作为 Identifier 呢?

事实证明,await 不能作为 Identifier,但是它可以 StringValue"await"(字符序列 awat 一种不同的表示方式) 的其他值。

标志符名称的静态语义定义了如何计算标志符名称的 StringValue。举个例子,a 的 Unicode 转义序列是 \u0061,因此 \u0061waitStringValue"await"。词法文法不会把 \u0061wait 当作关键字,而是当作一个 Identifier。静态语义禁止在异步函数中将 \u0061wait 作为变量名。

因此,这样的代码可以运行:

function old() {  var \u0061wait;}

这样的代码无法运行:

async function modern() {  var \u0061wait; // Syntax error}

总结 #

在这集内容中,我们了解了词法文法,语法文法,以及用于定义语法文法所用到的缩写。在示例中,我们了解到在异步方法中,await 不能作为标志符使用,但是在非异步方法中可以被当作标志符。

在下集内容中,会讲解关于语法文法其他有趣的部分,比如自动化插入逗号以及包含文法,敬请关注!