javascript自动分号插入(ASI)的规则是什么?

首先我应该问一下这是否依赖于浏览器。

我曾经读到过,如果发现了一个无效的令牌,但代码段在该无效令牌之前是有效的,如果令牌之前有换行符,则在令牌之前插入一个分号。

然而,常见的由分号插入引起的错误的例子是:

return
_a+b;

..这似乎不符合这个规则,因为_a将是一个有效的令牌。

另一方面,打破调用链可以正常工作:

$('#myButton')
.click(function(){alert("Hello!")});

有人对规则有更深入的描述吗?

289300 次浏览

首先,你应该知道哪些语句会受到自动分号插入(为简洁起见,也称为ASI)的影响:

  • 空语句
  • # EYZ0声明
  • 表达式语句
  • # EYZ0声明
  • # EYZ0声明
  • # EYZ0声明
  • # EYZ0声明
  • # EYZ0声明

ASI的具体规则在规范§11.9.1自动插入分号规则中描述

本文描述了三个案例:

  1. 当遇到语法不允许的违规标记时,在它前面插入分号,如果:
  • 令牌与前一个令牌之间至少隔一个LineTerminator
  • 令牌是}

# EYZ0:

    { 1
2 } 3

转化为

    { 1
;2 ;} 3;
NumericLiteral 1满足第一个条件,下面的令牌是行结束符。
2满足第二个条件,下面的令牌是}.

  1. 当遇到令牌输入流的末尾时,解析器无法将输入令牌流解析为一个完整的程序,则在输入流的末尾自动插入一个分号。

# EYZ0:

    a = b
++c

转换为:

    a = b;
++c;
  1. 这种情况发生在语法的某些产物允许使用令牌,但该产物是限制生产时,在受限制的令牌之前会自动插入一个分号。

受限制的产品:

    UpdateExpression :
LeftHandSideExpression [no LineTerminator here] ++
LeftHandSideExpression [no LineTerminator here] --
    

ContinueStatement :
continue ;
continue [no LineTerminator here] LabelIdentifier ;
    

BreakStatement :
break ;
break [no LineTerminator here] LabelIdentifier ;
    

ReturnStatement :
return ;
return [no LineTerminator here] Expression ;
    

ThrowStatement :
throw [no LineTerminator here] Expression ;


ArrowFunction :
ArrowParameters [no LineTerminator here] => ConciseBody


YieldExpression :
yield [no LineTerminator here] * AssignmentExpression
yield [no LineTerminator here] AssignmentExpression

经典的例子,使用ReturnStatement:

    return
"something";

转化为

    return;
"something";

直接来自ECMA-262,第五版ECMAScript规范:

7.9.1自动插入分号规则

分号的插入有三个基本规则:

    当从左到右解析程序时,遇到一个令牌(称为冒犯令牌),这是语法的任何结果都不允许的,如果以下一个或多个条件为真,则在有问题的令牌之前自动插入一个分号:
    • 有问题的令牌与前一个令牌之间至少隔了一个LineTerminator
    • 有问题的令牌是
    • 李< / ul > < / >
    • 当从左到右解析程序时,遇到标记输入流的末尾,解析器无法将输入标记流解析为单个完整的ECMAScript Program,则在输入流的末尾自动插入一个分号。
    • 当从左到右解析程序时,遇到语法的某些结果允许的令牌,但结果是限制生产,令牌将是受限制结果中紧跟注释“[这里没有LineTerminator]”之后的终端或非终端的第一个令牌(因此这样的令牌被称为受限制令牌),并且受限制令牌与前一个令牌至少用一个LineTerminator隔开,然后在受限制的令牌之前自动插入一个分号。

然而,在上述规则上有一个附加的覆盖条件:如果分号将被解析为空语句,或者如果分号将成为语句头中的两个分号之一,则永远不会自动插入分号(参见12.6.3)。

关于分号插入和var语句,注意在使用var但跨越多行时忘记使用逗号。有人昨天在我的代码中发现了这个:

    var srcRecords = src.records
srcIds = [];

它运行了,但结果是srcid声明/赋值是全局的,因为在前一行中带有var的局部声明不再应用,因为由于自动插入分号,该语句被认为已完成。

我不能很好地理解规范中的这3条规则——希望有一些更简单的英语——但以下是我从JavaScript: the Definitive Guide,第6版,David Flanagan, O'Reilly, 2011年收集到的内容:

引用:

JavaScript不会把每个换行符都当作分号:它通常只在没有分号时无法解析代码时才把换行符当作分号。

另一个引用:用于代码

var a
a
=
3 console.log(a)

JavaScript不把第二行换行当作分号,因为它可以继续解析更长的语句a = 3;

和:

当JavaScript不能将第二行解析为第一行语句的延续时,它会将换行符解释为分号,这是一般规则的两个例外。第一个异常涉及return、break和continue语句

... 如果在这些单词后出现换行符…JavaScript总是将换行符解释为分号。

... 第二个例外涉及到++和−−操作符…如果要将这些操作符中的任何一个用作后缀操作符,则它们必须与它们应用的表达式出现在同一行。否则,换行符将被视为分号,而++或——将被解析为应用于后面代码的前缀操作符。考虑下面的代码,例如:

x
++
y

它被解析为x; ++y;,而不是x++; y

所以我想简化一下,这意味着:

一般来说,JavaScript会将其视为代码的延续,只要它是有意义的——除了两种情况:(1)在一些关键字之后,如returnbreakcontinue,(2)如果它在新一行中看到++--,那么它会在前一行的末尾添加;

关于“只要有意义,就把它当作代码的延续”的部分;让它感觉像是正则表达式的贪婪匹配。

如上所述,这意味着对于带换行符的return, JavaScript解释器将插入;

(再次引用:如果在这些单词[如return]之后出现换行符,…JavaScript总是将换行符解释为分号)

由于这个原因,经典的例子

return
{
foo: 1
}

不会像预期的那样工作,因为JavaScript解释器会把它当作:

return;   // returning nothing
{
foo: 1
}

return之后不能有换行符:

return {
foo: 1
}

让它正常工作。如果你要遵循在任何语句后使用;的规则,你可以自己插入;:

return {
foo: 1
};

我所找到的关于JavaScript 自动插入分号的最相关的描述来自一本关于制作翻译的书。

JavaScript的“自动分号插入”规则是一个奇怪的规则。其他语言认为大多数换行符是有意义的,在多行语句中只有少数换行符应该被忽略,而JS则相反。除非遇到解析错误,否则它将所有换行符视为无意义的空格。如果是,则返回并尝试将前面的换行符转换为分号,以获得语法上有效的内容。

他继续描述了代码味道

如果我详细说明这是如何运作的,这篇设计说明就会变成一篇设计攻略,更不用说这是一个坏主意的各种方式了。真是一团糟。JavaScript是我所知道的唯一一种语言,许多风格指南要求在每个语句后显式地使用分号,尽管理论上该语言允许您省略分号。

补充一点,

const foo = function(){ return "foo" } //this doesn't add a semicolon here.
(function (){
console.log("aa");
})()

看这个,使用立即调用的函数表达式(IIFE)

JavaScript中的大多数语句和声明必须以分号结束,然而,为了程序员的方便(更少的输入,风格偏好,更少的代码噪音,更低的进入壁垒),在一些源文本位置可以省略分号,运行时根据规范中设置的一组规则自动插入分号。

全局规则:如果分号将被解析为空语句,或者该分号将成为for语句头中的两个分号之一,则永远不会自动插入分号。

规则1

如果JavaScript解析器遇到一个令牌(如果不存在分号,两个令牌都不允许),那么将自动插入分号,并且该令牌由一个或多个行终止符(例如。一个do-while循环的右括号}或最后的圆括号())。

换句话说:对于可运行的程序来说,语句总是需要终止的源文本位置,如果省略了语句结束符(;),则会自动插入该位置。这条规则是ASI的核心。

规则2

如果源文本不是有效的脚本或模块,则将在程序末尾插入分号。换句话说:程序员可以省略程序中的最后一个分号。

规则3

如果遇到一个令牌,如果分号不存在,则通常允许该令牌存在,但存在于几个特殊源文本位置之一(受限制的产品)中,这些位置为避免歧义而显式禁止在其中使用行终止符,则将自动插入分号。

禁止在其中使用行终止符的受限产品如下:

  • 在后缀++和后缀--之前(因此换行符后的一元增/减操作符将绑定到(不是之前的)语句,作为前缀操作符)
  • continuebreakthrowreturnyield之后
  • 后箭头函数参数列表,和
  • 在异步函数声明中的async关键字之后&表达式,生成器函数声明&表达式,方法和异步箭头函数

说明书包含了全部细节,加上以下实用的建议:

对ECMAScript程序员的实用建议是:

  • 后缀++或——操作符应该与其操作数在同一行。

  • return或throw语句中的表达式 yield表达式中的AssignmentExpression应该以相同的方式开始

    . line作为returnthrowyield标记
  • breakcontinue语句中的LabelIdentifier应该与breakcontinue令牌在同一行。

  • 箭头函数的参数的结尾和它的=>应该在同一行。

  • 异步函数或方法前面的async令牌应该与紧随其后的令牌在同一行。

# EYZ0。

ASI的例子

以“(”开始一行

开括号字符有多重含义。它可以描述表达式,也可以指示调用(当与右括号配对时)。

例如,下面的抛出&;Uncaught TypeError: console.log(…)不是一个函数&;因为运行时试图调用console.log('bar')的返回值:

let a = 'foo'
console.log('bar')
(a = 'bam')

一个解决方案是,如果你通常省略分号,包括一个分号,使你的意图明确:

let a = 'foo'
console.log('bar')
;(a = 'bam') // note semicolon at start of line

以“[”开始一行

开括号字符([)有多重含义。它可以指示对象属性访问,也可以指示数组的文字声明(当与右括号配对时),也可以指示数组析构。

例如,下面会抛出“Uncaught TypeError: Cannot set properties of undefined (setting 'foo')”;因为运行时试图在console.log('bar')的响应上设置名为'foo'的属性的值:

let a = 'foo'
console.log('bar')
[a] = ['bam']

一个解决方案是,如果你通常省略分号,包括一个分号,使你的意图明确:

let a = 'foo'
console.log('bar')
;[a] = ['bam'] // note semicolon at start of line