JavaScript ASI 机制详解

TL;DR

近来在清算 Pocket 的未读列表,看到了 An Open Letter to JavaScript Leaders Regarding Semicolons 才晓得了 JavaScript 的 ASI,一种自动插进去分号的机制。由于我是 “省略分号作风” 的支持者,之前也遇到过一次由于疏忽分号发生的题目,所以对此比较注意,也特地多看了几份文档,但越看内心越隐约。并非我记不住 ( 和 [ 前面记得加 ; 这类结论,而是以为看过的几篇文章跟 ECMAScript 规范形貌的有点区分。直到近来重复揣摩才倏忽有了 “原来如此” 的主意,因而就有了此文。

这篇文章会用 ECMAScript 规范的 ASI 定义来诠释它究竟是怎样运作的,我会只管用平易近民的要领形貌它,防止官方文档的艰涩。愿望你跟我一样有收成。控制 ASI 并不能够让你立时处置惩罚手头的题目,但能让你成为一个更好的 JavaScript 顺序员。

什么是 ASI

根据 ECMAScript 规范,一些 特定语句(statement) 必需以分号末端。分号代表这段语句的停止。然则有时刻为了轻易,这些分号是有能够省略的。这类状况下诠释器会本身推断语句该在那里停止。这类行动被叫做 “自动插进去分号”,简称 ASI (Automatic Semicolon Insertion) 。实际上分号并没有真的被插进去,这只是个便于诠释的抽象说法。

这些特定的语句有:

  • 空语句

  • let

  • const

  • import

  • export

  • 变量赋值

  • 表达式

  • debugger

  • continue

  • break

  • return

  • throw

下面这段是我 个人的邃晓,上的定义同时也示意:

  1. 所有这些语句中的分号都是能够省略的。

  2. 除此之外其他的语句有两种状况,一是不需要分号的(比方 if 和函数定义),二是分号不能省略的(比方 for),稍后会细致引见。

那末 ASI 怎样晓得在那里插进去分号呢?它会根据一些划定规矩去推断。但在说划定规矩之前,我们先相识一下 JS 是怎样剖析代码的。

Token

剖析器在剖析代码时,会把代码分红许多 token 。一个 token 相称于一小段有特定意义的语法片断。看一个例子你就会邃晓:

var a = 12;

上面这段代码能够分红四个 token :

  1. var 关键字

  2. a 标识符

  3. = 运算符

  4. 12 数字

除此之外,(. 等都算 token ,这里只是让你有个或许的观点,比方 12 全部是一个 token ,而不是 12。字符串同理。

诠释器在剖析语句时会一个一个读入 token 尝试构成一个完整的语句 (statement),直到遇到特定状况(比方语法划定的停止)才会以为这个语句完毕了。记得上文提到的 变量赋值 这个语句必需以分号末端么?这个例子中的停止符就是分号。用 token 构成语句的历程类似于正则里的贪欲婚配,诠释器老是试图用只管多的 token 构成语句。

接下来是重点:恣意 token 之间都能够插进去一个或多个换行符 (Line Terminator) ,这完整不影响 JS 的剖析,所以上面的代码能够写成下面如许(功用等价):

var
a
=
// = 和 12 之间有两个换行符
12
;

这个特征能够让开发者经由过程增添代码的可读性,更天真地构造言语作风。我们日常平凡写的跨多行的数组,字符串拼接,和链式挪用都属于这一类。不过在省略分号的作风中,这类剖析特征会致使一些不测状况。

比方这个例子中,以 / 开首的正则会被邃晓成除法:

var a
  , b = 12
  , hi = 2
  , g = {exec: function() { return 3 }}

a = b
/hi/g.exec('hi')

console.log(a)
// 打印出 2, 由于代码会被剖析成:
//   a = b / hi / g.exec('hi');
//   a = 12 / 2 / 3

事实上这并非省略分号的作风的毛病,而是开发者没有邃晓 JS 诠释器的事情道理。假如你偏向省略分号的作风,那相识 ASI 是必修课。

ASI 划定规矩

ECMAScript 规范定义的 ASI 包括 三条划定规矩两条破例

三条划定规矩是形貌什么时刻该自动插进去分号:

  1. 剖析器从左往右剖析代码(读入 token),当遇到一个不能构成正当语句的 token 时,它会在以下几种状况中在该 token 之前插进去分号,此时这个不合群的 token 被称为 offending token :

    • 假如这个 token 跟上一个 token 之间有最少一个换行。

    • 假如这个 token 是 }

    • 假如 前一个 token 是 ),它会试图把前面的 token 邃晓成 do...while 语句并插进去分号。

  2. 当剖析到文件末端发明语法照样有题目,就会在文件末端插进去分号。

  3. 当剖析时遇到 restricted production 的语法(比方 return),并且在 restricted production 划定的 [no LineTerminator here] 的处所发明换行,那末换行的处所就会被插进去分号。

两条破例示意,就算相符上述划定规矩,假如分号会被剖析成下面的模样,它也不能被自动插进去:

  1. 分号不能被剖析成空语句。

  2. 分号不能被剖析成 for 语句头部的两个分号之一。

你会发明这些划定规矩相称艰涩,彷佛居心考你智商的,还有些坑爹的专有名词。没关系,我们来看几个非常简朴的例子,看完以后你就会邃晓所有这些东西的寄义。

例子剖析

第一个例子:换行

a
b

我们模仿一下剖析器的思索历程,或许是如许的:剖析器一个个读取 token ,但读到第二个 token b 时它就发明没法构成正当的语句,然后它发明 b 和前面是有换行的,因而根据划定规矩一(状况一),它在 b 之前插进去分号变成 a\n;b,如许语句就正当了。然后继承处置惩罚,这时刻读到文件末了,b 照样不能构成正当的语句,这时刻候根据划定规矩二,它在末端插进去分号,完毕。终究效果是:

a
;b;

第二个例子:大括号

{ a } b

剖析器依然一个个读取 token ,读到 token } 时发明 { a } 是不正当的,由于 a 是表达式,它必需以分号末端。但当前 token 是 },所以根据划定规矩一(状况二),它在 } 前面插进去分号变成 { a ;},这句就经由过程了,然后继承处置惩罚,根据划定规矩二给 b 加上分号,完毕。终究效果是:

{ a ;} b;

顺带一提,或许有人会以为 { a; }; 如许才更天然。但 {...} 属于块语句,而根据定义块语句是不需要分号末端的,不论是不是是在一行。由于块语句也被用在其他处所(比方函数定义),所以下面这类代码也是完整正当的,不需要任何分号:

function a() {} function b() {}

第三个例子:do while

这个是为了诠释划定规矩一(状况三),这是最绕的部份,代码以下:

do a; while(b) c

这个例子中剖析到 token c 的时刻就不对了。这内里既没有换行也没有 },但 c 前面是 ),所以剖析器把之前的 token 构成一个语句,并推断该语句是不是是 do...while,效果正好是的!因而插进去分号变成 do a; while(b) ;,末了给 c 加上分号,完毕。终究效果为:

do a; while (b) ; c;

简朴点说,do...while 背面的分号是会自动插进去的。但假如其他以 ) 末端的状况就不行了。划定规矩一(状况三)就是为 do...while 量身定做的。

第四个例子:return

return
a

你肯定晓得 return 和返回值之间不能换行,由于上面代码会剖析成:

return;
a;

但为何不能换行?由于 return 语句就是一个 restricted production。这是什么意思?它是一组有严厉限制的语法的统称,这些语法都是在某个处所不能换行的,不能换行的处所会被标注 [no LineTerminator here]

比方 ECMAScript 的 return 语法定义以下:

return [no LineTerminator here] Expression ;

这示意 return 跟表达式之间是不允许换行的(但背面的表达式内部能够换行)。假如这个处所正好有换行,ASI 就会自动插进去分号,这就是划定规矩三的寄义。

适才我们说了 restricted production 是一组语法的统称,它一共包括下面几个语法:

  • 后缀的 ++--

  • return

  • continue

  • break

  • throw

  • ES6 箭头函数(参数和箭头之间不能换行)

  • yield

这些不必死记,由于根据通例誊写习气,险些没人会如许换行的。顺带一提,continuebreak 背面是能够接 label 的。但这不在本文议论范围内,有兴致能够本身探究。

第五个例子:后缀表达式

a
++
b

剖析器读到 token ++ 时发明语句不正当,由于后缀表达式是不允许换行的,换句话说,换行的都不是后缀表达式。所以它只能根据划定规矩一(状况一)在 ++ 前面加上分号来完毕语句 a,然后继承实行,由于前缀表达式并非 restricted production ,所以 ++b 能够构成一条语句,然后根据划定规矩二在末端加上分号。终究效果为:

a
;++
b;

第六个例子:空语句

if (a)
else b

诠释器剖析到 token else 时发明不正当,原本根据划定规矩一(状况一),它在应当加上分号变成 if (a)\n;,但如许 ; 就变成空语句了,所以根据破例一,这个分号不能加。顺序在 else 处抛非常完毕。Node.js 的运转效果:

else b
^^^^

SyntaxError: Unexpected token else

第七个例子:for

for (a; b
)

剖析器读到 token ) 时发明不正当,原本换行能够自动插进去分号,但根据破例二,不能为 for 头部自动插进去分号,因而顺序在 ) 处抛非常完毕。Node.js 运转效果以下:

)
^

SyntaxError: Unexpected token )

怎样手动测试 ASI

我们很难有方法去测试 ASI 是不是是如预期那样事情的,只能看到代码终究实行效果是对是错。ASI 也没有手动翻开或关掉去对照效果。但我们能够经由过程对照剖析器天生的 tree 是不是一致来推断 ASI 加的分号是不是是跟我们预期的一致。这点能够用 Esprima 在线剖析器 完成。

拿这段代码举例子:

do a; while(b) c

Esprima 剖析的 Syntax 以下所示(不需要看懂,记着或许模样就行):

{
    "type": "Program",
    "body": [
        {
            "type": "DoWhileStatement",
            "body": {
                "type": "ExpressionStatement",
                "expression": {
                    "type": "Identifier",
                    "name": "a"
                }
            },
            "test": {
                "type": "Identifier",
                "name": "b"
            }
        },
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "Identifier",
                "name": "c"
            }
        }
    ],
    "sourceType": "script"
}

然后我们把加上分号的版本输入进去:

do a; while(b); c;

你会发明天生的 Syntax 是一致的。这说明诠释器对这两段代码剖析历程是一致的,我们并没有到场任何过剩的分号。

然后尝尝这个有过剩分号的版本:

do a; while(b); c;; // 末端多一个分号

Esprima 效果:

{
    "type": "Program",
    "body": [
        {
            "type": "DoWhileStatement",
            "body": {
                "type": "ExpressionStatement",
                "expression": {
                    "type": "Identifier",
                    "name": "a"
                }
            },
            "test": {
                "type": "Identifier",
                "name": "b"
            }
        },
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "Identifier",
                "name": "c"
            }
        },
        {
            // 多出来一个空语句
            "type": "EmptyStatement"
        }
    ],
    "sourceType": "script"
}

你会发明多出来一条空语句,那末这个分号就是过剩的。

末端

假如看到这里,相信你对 ASI 和 JS 的剖析机制已有所相识。或许你会想 “那我不再省略分号了”,那我发起你看看参考资料里的链接。而且就我的履历,即使是分号的对峙者,少数处所也会无意识地运用 ASI 。比方有时刻忘了写分号,或许写迭代器中的单行函数时。下次我会说下对省略分号的作风的意见,和怎样用 ESLint 保证代码作风的一致性。

参考资料

ECMAScript: ASI
ECMAScript 规范定义。本文的观点和许多例子完整遵循它来写的。但也强烈发起你本身看看。

JavaScript Semicolon Insertion Everything you need to know
关于 ASI 的诠释,稍微学术化,讲得很细致,也很客观。

An Open Letter to JavaScript Leaders Regarding Semicolons
NPM 作者对 ASI 和两种作风的意见,这篇更注意个人观点的表达。他是省略分号作风的偏向者。

Esprima: Parser
一个在线 JS 剖析器。你能够输入一些语句来看看 token 都是什么。也能够经由过程 Tree 的变化来测试加不加分号的影响。

    原文作者:darkbaby123
    原文地址: https://segmentfault.com/a/1190000004548664
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞