Home

blackshh

纸上得来终觉浅,绝知此事要躬行。

Blog About Email Github

了解JavaScript分号插入机制的局限性

JavaScript的一个便利是能够离开语句结束分号工作。删除分号后,语句变得轻量而优雅。

function Ponit(x,y){
    this.x = x || 0
    this.y = y || 0
}

上面的代码能工作多亏JavaScript的自动分号插入(automatic semicolon insertion)技术,它是一种程序解析技术。它能推断出某些上下文中省略的分号,然后有效地自动地将分号“插入”到程序中。ECMAScript标准细心地制定了分号插入机制,因此,可选分号可以在不同的JavaScript引擎之间移植。
但是同JavaScript的隐式强制转换一样,分号插入也有其陷阱,你根本不能避免学习其规则。即使你从来不省略分号,受分号插入的影响,JavaScript语法也有一些额外的限制。好消息是,一旦你学会分号插入的规则,你会发现你能从删除不必要的分号的痛苦中解脱出来。

分号插入的第一条规则:

分号仅在}标记之前、一个或多个换行之后和程序输入的结尾被插入。
换句话说,你只能在一行、一个代码块和一段程序结束的地方省略分号。因此,下面的函数定义是合法的。

function add(a,b){
    var sum = a+b
    return sum
}
function area(r){ return Math.PI * r * r }

但是,下面这个却不合法。

function add(a,b){ var sum = a+b return sum } //error

分号插入的第二条规则:

分号仅在随后的输入标记不能解析时插入。
换句话说,分号插入是一种错误校正机制。下面这段代码作为一个简单的例子。

a = b
(f());

能正确地解析为一条单独的语句,等价于:

a = b(f());

也就是说,没有分号插入。与此相反,下面这段代码:

a = b
f();

被解析为两条独立的语句,因为

a = b f();

解析有误。
这条规则有一个不幸的影响:你总是要注意下一条语句的开始,从而发现你是否能合法地省略分号。如果某条语句的下一行的初始标记不能被解析为一个语句的延续,那么,你不能省略该条语句的分号。
有5个明确有问题的字符需要密切注意:(、 [、 +、 -、 和 /。每一个字符都能作为一个表达式运算符或者一条语句的前缀,这依赖于具体上下文。因此,要小心提防那些以表达式结束的语句,就像上面的赋值语句一样。如果下一行以这5个有问题的字符之一开始,那么不会插入分号。到目前为止,最常见的情况是以一个括号开始,就像上面的例子。

-以“(”开头的情况:

a = b
(function() {

})()

javascript会解释成:


a = b(function() {

})();

以“[”开头的情况

a = function() {

}
[1,2,3].forEach(function(item) {

});

javascript会解释成:

a = function() {
}[1,2,3].forEach(function(item) {

});

以“/”开头的情况

a = 'abc'
/[a-z]/.test(a)

期望的结果为true,但是javascript会解释成,接着就报错了:

a = ‘abc’/[a-z]/.test(a);

以“+”开头的情况

a = b
+c

javascript会解释成

a = b + c;

以“-”开头的情况

a = b
-c

javascript会解释成

a = b - c;

另一个常见的情况是,省略分号可能导致脚本连接问题。每个文件可能由大量的函数调用表达式(立即执行函数)组成。

//file1.js
(function(){
    //...
})()

//file2.js
(function(){
    //...
})()

当每个文件作为一个单独的程序加载时,分号能自动地插入到末尾,将函数调用转变为一条语句。 但是,当这些文件以下面的方式进行连接时:

(function(){
    //...
})()
(function(){
    //...
})()

结果被视为一条单一的语句,等价于:

(function(){
    //...
})()(function(){ 
    //...
})()

结果是:省略语句的分号不仅需要当心当前文件的下一个标记,而且还需要当心脚本连接后可能出现在语句之后的任一标记。类似上述方法,你可以防御性地为每个文件前缀一个额外的分号以保护脚本免受粗心连接的影响。如果文件最开始的语句以这5个脆弱的字符(、[、+、-和 /开头,你就应该这么做。

//file1.js
;(function(){
    //...
})()

//file2.js
;(function(){
    //...
})()

这会确保即使前一个文件忽略了最后的分号,合并后的结果仍然会被视为单独的语句。

;(function(){
    //...
})()
;(function(){
    //...
})()

当然,如果脚本连接程序能够自动地在文件之间增加额外的分号是更好的。但并不是所有的脚本连接工具都写得很好,因此,最安全的选择是防御性地增加分号。
此时,你可能会认为,“这是多余的担心。我从来就不省略分号,我会没事儿的。”事实并不是这样。也有一些情况,尽管不会出现解析错误,JavaScript仍会强制地插入分号。这就是所谓的JavaScript语法限制产生式(restricted production),它不允许在两个字符之间出现换行。最危险的情况是return语句,在return关键字和其可选参数之间一定不能包含换行符。因此,语句

return { };

返回一个新对象,而下面这段代码

return
{ };

被解析为3条单独的语句,等价于:

return;
{ }
;

换句话说,return关键字后的换行会强制自动地插入分号。该段代码被解析为不带参数的return语句,后接一个空的代码块和一条空语句。其他的限制产生式包括:

  • throw语句
  • 带有显式标签的break或continue语句
  • 后置自增或自减运算符

第三条也是最后一条分号插入规则:

分号不会作为分隔符在for循环空语句的头部被自动插入。
这就意味着你必须在for循环头部显式地包含分号。否则,类似下面的代码

for(var i = 0 
i < 10
i++){
    console.log(i)
}

将会导致解析错误。空循环体的while循环同样也需要显式的分号。否则,省略分号也会导致解析错误。

参考资料

  • 编写高质量的JavaScript的68个有效方法

blackshh

2018-02-01

Blog About Email Github