导读
JS代码需不需要加分号,有这个问题就证明JS加不加分号都行,加了会减轻编译器的工作负担,不加编译器会自动补全分号,减轻开发者的负担。
编译器在处理JS代码的时候,找到分号就直接当语句结束,没找到会按照特定规则对分号进行补全。本文主要内容为:
- 分号补全规则具体细节
- 不加分号可能会遇到的问题以及解决方案
分号自动补全规则
主要规则如下:
-
如果遇到换行符,且下一个符号是不符合语法的,会尝试插入分号;
-
如果遇到换行符,且规定此处不能有换行符,会自动插入分号;
-
源代码结束的时候,不能形成完整的脚本或者模块结构,会自动插入分号。
下面举几个例子🌰;
例子1:
对应规则1
const foo = 1 /* 这里不加分号 */
void function(foo){
console.log(foo);
}(foo);
第一句let foo = 1
后面有换行符,然后遇到void
关键字,不符合语法规则,所以会在void
前面加分号。
例子2:
对应规则2
const foo = 1;
const bar = 1;
const baz = 1;
foo /* 这里不加分号 */
++ /* 这里不加分号 */
bar /* 这里不加分号 */
++ /* 这里不加分号 */
baz /* 这里不加分号 */
foo++
是合法的,但是JS规定(这里的规定后面会细说),foo
和++
之间不能加换行符,所以会在foo
后面加入分号,最后代码相当会变成这样:
const foo = 1;
const bar = 1;
const baz = 1;
foo;
++bar;
++baz;
// foo: 1, bar: 2, baz: 2
对于规则3,加不加分号都无所谓,反正是在代码最后面,不影响前面代码的执行,所以就不举例子了。
不能有换行符的语法
在JS标准中,有个叫 [no LineTerminator here] 的规则,规定哪些语法不能加入换行符,如果开发者加了换行符,则JS编译器会无法识别并加入分号。
-
带标签的
continue
语句,不能continue
后插入换行; -
带标签的
break
语句,不能在break
后面插入换行; -
return
后面不能插入换行; -
后自增、后自减运算符前不能插入换行;
-
throw
和Exception
之间不能插入换行; -
async
关键字,后面不能插入换行; -
箭头函数的箭头前,不能插入换行;
-
yield
之后,不能插入换行。
下面用代码展示一下:
带标签的continue
语句,不能continue
后插入换行:
tag:for(var j = 0; j < 10; j++)
for(var i = 0; i < j; i++)
continue /*no LineTerminator here*/tag
带标签的break
语句,不能在break
后面插入换行:
tag:for(var j = 0; j < 10; j++)
for(var i = 0; i < j; i++)
break /*no LineTerminator here*/tag
return
后面不能插入换行:
function sum(a, b) {
return /*no LineTerminator here*/a + b;
}
后自增、后自减运算符前不能插入换行:
i/*no LineTerminator here*/++
i/*no LineTerminator here*/--
throw
和Exception
之间不能插入换行:
throw /*no LineTerminator here*/new Exception("error")
async
关键字,后面不能插入换行:
async /*no LineTerminator here*/function foo(){
}
const foo = async /*no LineTerminator here*/x => x*x
箭头函数的箭头前,不能插入换行:
const foo = x /*no LineTerminator here*/=> x*x
yield
之后,不能插入换行:
function *g(){
var i = 0;
while(true) {
yield /*no LineTerminator here*/i++;
}
}
以上所有情况都会在JS执行时在/*no LineTerminator here*/
处加上分号。但是JS这些规则还不够完善,会有几种情况不符合预期,需要开发者而外注意。
不加分号需要注意的情况
下面列举几个不加分号需要注意的情况。
以括号开头的语句
(function(a){
console.log(a);
})()/*这里没有被自动插入分号*/
(function(b){
console.log(b);
})()
这里开发者的意图是想执行两个立即执行函数( IIFE ),但是()
后面还是()
,在JS看来是合法的语句(如:函数返回一个函数再调用),所以这里不会被自动加入分号,第二个 IIFE 会被当做第一个 IIFE 执行完之后继续执行的入参。然后由于第一个 IIFE 没有返回函数,这里会抛出一个xxx is not a function
的类型错误。
以数组开头的语句
const arr = [[1, 2]]/*这里没有被自动插入分号*/
[3, 2, 1, 0].forEach(item => console.log(item))
这里开发者的意图是想给arr
遍历赋值,然后遍历一个数组打印每一项的值,但是由于不会自动加分号,这里会被解读成下表运算符和逗号运算符,并且不会报错!!!
代码解读:首先[3, 2, 1, 0]
会被当做[[1, 2]]
的下标,然后3, 2, 1, 0
执行逗号运算符的结果是0,最后代码等价于:
const arr = [[1, 2]][0].forEach(item => console.log(item)) // 1, 2
结果完全不符合预期,还不报错,调试起来会异常困难。
以正则表达式开头的语句
这个例子让我想到了XSS攻击,利用语法规则对代码解读进行干预,从而窃取用户信息。
const x = 1/*这里没有被自动插入分号*/
/(a)/g.test("abc")
console.log(RegExp.$1)
这里开发者的意图是给变量x
赋值,并打印正则匹配的结果。但是由于不会自动加分号,正则表达式的第一个/
会被当成除号,1
➗(a)/g.test("abc")
结果为NaN
,所以实际没有执行test
函数。
这里的函数也不会报错,找起问题来会很dan疼。
以 Template 开头的语句
这种情况比较少见,在和正则配合使用的时候会出现问题。
const f = function(){
return "";
}
const g = f/*这里没有被自动插入分号*/
`Template`.match(/(a)/);
console.log(RegExp.$1)
这里开发者的意图是将函数f
赋值给函数g
,然后执行match
方法,查看正则匹配的值。但是由于不会自动加分号,f
会和Template
连起来解读,作为f
的参数传入。
注意,函数是可以跟着模板字符串表示执行函数,字符串内容会被保存到arguments
中。
var f = function(){
console.log(arguments);
}
f`Template` // ['Template']
f`Template${1}` // ['Template', '1']
如何解决上述问题
最保险的方法当然是自己加分号。
如果你实在不想加,可以利用一些工具自动补全分号,如配合eslint
和vs code
一起使用,在保存的时候根据规则自动补充分号:
eslintrc
中加入这条规则:"semi": ["error", "always"]
vs code
中设置一下:"eslint.autoFixOnSave": true
也可以直接在eslint
中限制写法,从源头解决问题。如:使用no-plusplus限制自增符号的使用,用+=
的方式替代。
结语
以上就是本文的全部内容,个人觉得其实加不加分号都行,主要团队内要统一,别一会儿加一会儿不加。而且需要注意的规则也不多,遇到过几次估计都记住了。最后希望大家看完之后有所收获,将bug扼杀在摇篮里。