第2081期理解ECMAScript规范

Posted 前端早读课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第2081期理解ECMAScript规范相关的知识,希望对你有一定的参考价值。

前言

今日早读文章由@李松峰翻译授权分享。

正文从这开始~~

这一次我们深入ECMAScript语言及其语法的定义。如果你不太熟悉上下文无关文法,应该先补补课,至少先弄懂一些基本概念。因为规范中使用了上下文无关文法定义语言。

ECMAScript文法

ECMAScript规范定义了4种文法。

  • 词法文法:描述怎么把Unicode码点(code point)翻译为输入元素(标记、行终止符、注释、空白)序列。

  • 语法文法:定义标记(token)怎么构成语法正确的程序。

  • 正则文法:描述怎么把Unicode码点翻译为正则表达式。

  • 数值字符串文法:描述怎么把String翻译成数字值。

每种文法都用上下文无关文法来定义,都包含一组产生式。

不同的文法使用了不同的表示方式。语法文法表示为LeftHandSideSymbol :,词法文法和正则文法表示为LeftHandSideSymbol ::,而数值字符串文法表示为LeftHandSideSymbol :::。(以冒号的多少来区分。——译者注)

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

词法文法

规范将ECMAScript源文本定义为一个Unicode码点序列。这意味着变量名并不限于ASCII字符,也可以包含其他Unicode字符。规范并没有谈到实际的编码(如UTF-8或UTF-16),而是假设源代码已经按照自己的编码转换成了Unicode码点序列。

无法提前对ECMAScript源码进行标记化(tokenize),这使得定义词法文法略显复杂。比如,如果不看它所处的更大的上下文,就无法确定/是除法操作符还是正则表达式的开始。

 
   
   
 
  1. const x = 10 / 5;

这里的/是DivPunctuator 。

 
   
   
 
  1. const r = /foo/;

这里/是RegularExpressionLiteral的开头。

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

 
   
   
 
  1. const what1 = 'temp';

  2. const what2 = 'late';

  3. const t = `I am a ${ what1 + what2 }`;

这里I am a ${是TemplateHead,而}`是TemplateTail。

 
   
   
 
  1. if (0 == 1) {

  2. }`not very useful`;

这里}是RightBracePunctuator ,而`是NoSubstitutionTemplate的开头。

即便对/和}`的解释取决于上下文(它们在代码语法结构中的位置),我们下面介绍的文法仍然是上下文无关的。

词汇文法使用一些目标符号(goal symbol)来区分哪些上下文允许哪些输入元素,不允许哪些输入元素。例如,目标符号InputElementDiv(注意,这里的Div是Divide,即除法的意思。——译者注)会用在/是除法和/=是除法赋值的上下文中。InputElementDiv产生式列出了在此上下文中可能产生的标记:

 
   
   
 
  1. InputElementDiv ::

  2. WhiteSpace

  3. LineTerminator

  4. Comment

  5. CommonToken

  6. DivPunctuator

  7. RightBracePunctuator

在这个上下文中,遇到/产生输入元素DivPunctuator,而不会产生RegularExpressionLiteral。

相应地,对于/是正则表达式开头的上下文,目标符号是InputElementRegExp:

 
   
   
 
  1. InputElementRegExp ::

  2. WhiteSpace

  3. LineTerminator

  4. Comment

  5. CommonToken

  6. RightBracePunctuator

  7. RegularExpressionLiteral

这个产生式可以产生RegularExpressionLiteral输入元素,但不可能产生DivPunctuator。

类似地,目标符号InputElementRegExpOrTemplateTail对应的上下文除了RegularExpressionLiteral,还允许出现TemplateMiddle和TemplateTailt。最后一个InputElementTemplateTail目标符号的上下文只允许TemplateMiddle和TemplateTail,不允许出现RegularExpressionLiteral。

在实现中,语法文法分析器(“解析器”)可以调用词法文法分析器(“标记器”或“词法器”),将目标符号作为参数传递,并请求适合该目标符号的下一个输入元素。

语法文法

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

例子:允许遗留标识符

给文法增加新关键字有可能造成破坏:如果原有代码已经使用该关键字作为标识符了怎么办?

例如,在await还不是关键字的时候,可能出现这样的代码:

 
   
   
 
  1. function old() {

  2. var await;

  3. }

ECMAScript文法谨慎地添加了await关键字,以便这段代码可以继续工作。在async函数里面,await是一个关键字,所以不能这样写:

 
   
   
 
  1. async function modern() {

  2. var await; // 语法错误

  3. }

在非生成器中允许yield,而在生成器中不允许与此类似。

要理解怎么允许await作为标识符,需要理解ECMAScript特定的语法文法表示。

产生式与简写形式

来看看VariableStatement的产生式是怎么定义的。乍一看,这个文法有点吓人:

 
   
   
 
  1. VariableStatement[Yield, Await] :

  2. var VariableDeclarationList[+In, ?Yield, ?Await] ;

这里的下标([Yield, Await])和前缀(+In里的+和?Await里的?)都什么意思呀?

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

下标是对一组产生式的简写形式,一次性表达了一组产生式左端(left-hand side)符号。这个产生式左端符号有两个参数,它们可以展开为四个“真正的”产生式左端符号:

  • VariableStatement

  • VariableStatement_Yield

  • VariableStatement_Await

  • VariableStatementYieldAwait

注意,以上VariableStatement就表示VariableStatement,没有Await也没有Yield。不要把它跟VariableStatement[Yield, Await](简写形式)弄混了。

在产生式右端,可以看到简写+In,意思是“使用带In的版本”,而?Await的意思是“当且仅当左端符号有Await时使用带_Await的版本”(?Yield也是类似的)。

第三种简写形式~Foo,意思是“使用没有_Foo的版本”(这个产生式中没有出现)。

了解了这些,可以把上面的产生式展开成这样:

 
   
   
 
  1. VariableStatement :

  2. var VariableDeclarationList_In ;


  3. VariableStatement_Yield :

  4. var VariableDeclarationList_In_Yield ;


  5. VariableStatement_Await :

  6. var VariableDeclarationList_In_Await ;


  7. VariableStatement_Yield_Await :

  8. var VariableDeclarationList_In_Yield_Await ;

最后,还有要搞清楚两件事。

  • 在哪里确定我们处在有Await或没有Await的情况下?

  • 有它和没它有什么区别,或者说SomethingAwait的产生式和Something(没有Await)的产生式是在哪里分叉的?

Await还是没有Await

先解决第一个问题。因为很容易猜到可以根据函数体是否带_Await来区分异步函数和非异步函数。看异步函数声明的产生式,我们发现了这个:

 
   
   
 
  1. AsyncFunctionBody :

  2. FunctionBody[~Yield, +Await]

注意AsyncFunctionBody没有参数,参数在右端被添加给了FunctionBody。展开这个产生式得到:

 
   
   
 
  1. AsyncFunctionBody :

  2. FunctionBody_Await

换句话说,异步函数有FunctionBody_Await(即一个await会被当成关键字的)函数体。

另一方面,如果是在非异步函数中,相关产生式为:

 
   
   
 
  1. FunctionDeclaration[Yield, Await, Default] :

  2. function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }

(FunctionDeclaration还有一个产生式,但跟我们的代码示例不相关。)

为避免组合展开,我们忽略Default参数,因为这个特别的产生式中没用到。于是,这个产生式的展开形式为:

 
   
   
 
  1. FunctionDeclaration :

  2. function BindingIdentifier ( FormalParameters) { FunctionBody }


  3. FunctionDeclaration_Yield :

  4. function BindingIdentifier_Yield ( FormalParameters) { FunctionBody }


  5. FunctionDeclaration_Await :

  6. function BindingIdentifier_Await ( FormalParameters) { FunctionBody }


  7. FunctionDeclaration_Yield_Await :

  8. function BindingIdentifier_Yield_Await ( FormalParameters) { FunctionBody }

在这个产生式中,只有FunctionBody和FormalParameters(不带Yield,也不带Await),因为在未展开的产生式中它们都有参数[~Yield, ~Await]。

函数名的处理方式就不一样了:如果产生式左端符号中包含Yield和Await,则右端的函数名也会带上相应参数。

总结:异步函数有FunctionBodyAwait,而非异步函数有FunctionBody(不带Await)。因为我们讨论的是非生成器函数,所以无论异步示例函数还是非异步示例函数,都不会带参数_Yield。

可能记住哪个是FunctionBody,哪个是FunctionBodyAwait有点难。在有FunctionBodyAwait的函数体中,await是标识符,还是关键字?

可以把Await参数的意思理解为“await是一个关键字。”这样理解在将来也不会有问题。假设将来又添加了一个blob关键字,但只适用于“斑驳”(blobby)函数。非斑驳、非异步、非生成器的函数仍然有FunctionBody(不带Yield、Await或Blob),跟现在完全一样。斑驳函数则会有FunctionBodyAwaitBlob等。虽然仍然要在产生式中添加Blob下标,但已存在函数的FunctionBody的展开形式还跟以前一样。

不允许await用作标识符

接下来,我们要搞清楚在FunctionBody_Await中,是怎么不允许await用作标识符的。

仔细看一看产生式,可以发现从FunctionBody到之前的VariableStatement产生式都带Await参数。因此,在异步函数中,就有VariableStatementAwait,而在非异步函数中,则有VariableStatement。

再看仔细一点,注意一下参数。我们已经看到了这个VariableStatement的产生式:

 
   
   
 
  1. VariableStatement[Yield, Await] :

  2. var VariableDeclarationList[+In, ?Yield, ?Await] ;

所有VariableDeclarationList的产生式也都照样带这些参数:

 
   
   
 
  1. VariableDeclarationList[In, Yield, Await] :

  2. VariableDeclaration[?In, ?Yield, ?Await]

(这里只展示与我们例子相关的产生式。)

 
   
   
 
  1. VariableDeclarationList[In, Yield, Await] :

  2. BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]opt ;

这里的opt简写代表产生式右端符号是可选的,也就是实际上有两个产生式:一个带可选的符号(Initializer),另一个不带。

对我们简单的例子来说,VariableStatement包含关键字var,紧跟一个BindingIdentifier(没有初始化器Initializer),以分号结束。

为了允许或不允许await用作BindingIdentifier,我们希望最终看到这些:

 
   
   
 
  1. BindingIdentifier_Await :

  2. Identifier

  3. yield


  4. BindingIdentifier :

  5. Identifier

  6. yield

  7. await

这表示在异步函数中不允许await作为标识符,在非异步函数允许它作为标识符。

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

 
   
   
 
  1. BindingIdentifier[Yield, Await] :

  2. Identifier

  3. yield

  4. await

展开后得到:

 
   
   
 
  1. BindingIdentifier_Await :

  2. Identifier

  3. yield

  4. await


  5. BindingIdentifier :

  6. Identifier

  7. yield

  8. await

(这里省略了BindingIdentifierYield和BindingIdentifierYield_Await的产生式,因为我们的例子不需要。)

这么看await和yield任何时候都可以作为标识符。这是怎么回事啊?这篇文章难道白写了吗?

静态语义出马

原来为了在异步函数中禁止将await用作标识符,还需要用到静态语义。

静态语义描述静态规则,也就是在程序运行前要校验的规则。

对我们的例子而言,BindingIdentifier的静态语义定义了以下语法导向的规则:

 
   
   
 
  1. BindingIdentifier[Yield, Await] : await

如果这个产生式有[Await]参数就是一个语法错误(Syntax Error)。

实际上,这就是禁止BindingIdentifier_Await : await产生式。

规范解释说,之所以存在这个产生式但又通过静态语义将它定义为语法错误,是因为与ASI(Automatic Semicolon Insertion,自动插入分号)冲突。

我们知道,在无法根据语法产生式解析一行代码时,ASI就会介入。ASI尝试添加分号以满足语句和声明必须以分号结束的要求。(关于ASI的详细介绍,可以看下一篇文章。)

来看下面的代码(规范中的例子):

 
   
   
 
  1. async function too_few_semicolons() {

  2. let

  3. await 0;

  4. }

如果文法不允许await作用标识符,ASI就会介入并将上面的代码转换为下面这样文法正确的代码,这样let也会被当成标识符:

 
   
   
 
  1. async function too_few_semicolons() {

  2. let;

  3. await 0;

  4. }

与ASI的这种冲突被认为太令人困惑,因此才用静态语义禁止await作为标识符。

不允许标识符的StringValues

还有另一条相关规则:

 
   
   
 
  1. BindingIdentifier : Identifier

如果这个产生式有[Await]参数,且Identifier的StringValue是"await",就是一个语法错误(Syntax Error)。

乍一看不好理解。Identifier的定义如下:

 
   
   
 
  1. Identifier :

  2. IdentifierName but not ReservedWord

await是个ReservedWord,因此Identifier怎么可能是await呢?

的确,Identifier不可能是await,但可以是StringValue为"await"(字符序列await的一种不同的表示方式)的其他值。

标识符名的静态语义定义了如何计算标识符的StringValue。比如,a的Unicode转义序列是\u0061,因此\u0061wait的StringValue是"await"。\u0061wait不会被词法语法识别为关键字,而会被识别为Identifier。静态语义禁止在异步函数中使用它作为变量名。

因此,这样可以:

 
   
   
 
  1. function old() {

  2. var \u0061wait;

  3. }

但这样不行:

 
   
   
 
  1. async function modern() {

  2. var \u0061wait; // 语法错误

  3. }

小结

通过学习这篇文章,我们了解了词法文法、语法文法,以及用于定义语法文法的简写形式。作为例子,我们研究了await在异步函数中被禁止用作标识符,但在非异步函数中则允许。

理解ECMAScript规范系列文






最后,对着有情怀的javascript高级程序4,你有兴趣了解么?

添加早读君微信: zhgb_f2er,领取购书优惠券,数量有限

以上是关于第2081期理解ECMAScript规范的主要内容,如果未能解决你的问题,请参考以下文章

第2079期理解ECMAScript规范

第2083期理解ECMAScript规范

第800期 ECMAScript 2016 中你不知道的改变

第1006期ECMAScript 6 新特性

第1248期ECMAScript 2016, 2017, 和2018中新增功能

优化选址基于matlab帝国企鹅算法求解工厂-中心-需求点三级选址问题含Matlab源码 2081期