JS代码是否需要加分号

919 阅读5分钟

导读

JS代码需不需要加分号,有这个问题就证明JS加不加分号都行,加了会减轻编译器的工作负担,不加编译器会自动补全分号,减轻开发者的负担。

编译器在处理JS代码的时候,找到分号就直接当语句结束,没找到会按照特定规则对分号进行补全。本文主要内容为:

  • 分号补全规则具体细节
  • 不加分号可能会遇到的问题以及解决方案

分号自动补全规则

主要规则如下:

  1. 如果遇到换行符,且下一个符号是不符合语法的,会尝试插入分号;

  2. 如果遇到换行符,且规定此处不能有换行符,会自动插入分号;

  3. 源代码结束的时候,不能形成完整的脚本或者模块结构,会自动插入分号。

下面举几个例子🌰;

例子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编译器会无法识别并加入分号。

  1. 带标签的continue语句,不能continue后插入换行;

  2. 带标签的break语句,不能在break后面插入换行;

  3. return后面不能插入换行;

  4. 后自增、后自减运算符前不能插入换行;

  5. throwException之间不能插入换行;

  6. async关键字,后面不能插入换行;

  7. 箭头函数的箭头前,不能插入换行;

  8. 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*/--

throwException之间不能插入换行:

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']

如何解决上述问题

最保险的方法当然是自己加分号。

如果你实在不想加,可以利用一些工具自动补全分号,如配合eslintvs code一起使用,在保存的时候根据规则自动补充分号:

  1. eslintrc中加入这条规则:"semi": ["error", "always"]
  2. vs code中设置一下:"eslint.autoFixOnSave": true

也可以直接在eslint中限制写法,从源头解决问题。如:使用no-plusplus限制自增符号的使用,用+=的方式替代。

结语

以上就是本文的全部内容,个人觉得其实加不加分号都行,主要团队内要统一,别一会儿加一会儿不加。而且需要注意的规则也不多,遇到过几次估计都记住了。最后希望大家看完之后有所收获,将bug扼杀在摇篮里。

升职加薪被指到的人 - 被指到的人 升职加薪 心想事成 顺顺利利 好运连连 健健康康 一直开心 发财暴富