"要不要写分号?"是前端圈最久经不衰的争论。
与其站队,不如把自动插入分号(ASI, Automatic Semicolon Insertion)机制吃透 ,知道它什么时候可靠、什么时候会坑,然后给出一套工程可落地的规约与工具配置。本文就是你的一次性指南。
一、结论先行(速读篇)
-
团队统一即可 :写 or 不写都行,但必须统一并用工具固化(Prettier/ESLint)。
-
不写分号也安全 :多数情况下 ASI 能正常工作,但存在少数"致命边界" ,需要有意识规避。
-
这 4 类语句行首务必加分号或改写:
- 以
(
开头 ------ 可能与上行形成 IIFE 调用 - 以
[
开头 ------ 可能被当作上行表达式的下标/逗号表达式 - 以
/
(正则)开头 ------ 可能被解析为除号 - 以 `````(模板字面量)开头 ------ 可能与上一行函数/变量粘连
- 以
一句话建议:若采用无分号风格,行首遇到
([/
,请特别留意或在上一行末尾显式补一个分号。
二、ASI 的三条铁律(记住就够了)
自动插入分号与语法产生式并列存在,核心就 3 条:
- 行尾有换行 且下一个 token 放在当前行会不合法 → 引擎会尝试在行尾插入分号。
- 语法明确要求"此处不得换行"(
[no LineTerminator here]
) ,一旦出现换行 → 自动插号。 - 到达源文件末尾 仍无法形成完整的脚本/模块结构 → 末尾自动插号。
这三条解释了大多数"为什么这里不写分号也能跑"的场景;同时也给出了"哪里不能依赖 ASI"的线索。
三、[no LineTerminator here]
到底约束了谁?
语法中的该标记,表示这俩 token 之间不允许换行。遇到换行,引擎会自动插入分号把前后语句拆开。高频点如下(背下来):
-
后置自增/自减:
cssi /* no LT here */ ++ i /* no LT here */ --
-
return
、throw
、yield
、await
、async
后面紧跟的实体:javascriptreturn /* no LT here */ value throw /* no LT here */ new Error() yield /* no LT here */ i++ async /* no LT here */ function f() {} const f = async /* no LT here */ x => x * x
-
箭头函数箭头前:
iniconst f = x /* no LT here */ => x * x
-
带标签的
break/continue
与标签名之间:kotlinbreak /* no LT here */ outer continue/* no LT here */ outer
⚠️ 经典坑:
csharp
function f() {
return
1
}
// 实际等价于 return; 1; → 返回 undefined
四、最容易翻车的 4 类"行首"写法
只要你走"无分号"风格,下面四类行首必须提高警惕。
1)以 (
开头:IIFE 粘连
javascript
// 期望两个独立的 IIFE
(function () { console.log('A'); })()
(function () { console.log('B'); })()
// 但如果上一行被解析为"返回函数",这里的 () 会被当成"继续调用上一个结果"
安全写法:在第二个 IIFE 前加分号或换成显式声明。
javascript
(function () { console.log('A'); })();
;(function () { console.log('B'); })()
2)以 [
开头:下标/逗号表达式粘连
lua
const a = [[]]
[3, 2, 1].forEach(console.log)
// 可能被理解为:a[3,2,1].forEach(...) ------ 语义跑偏且不一定立刻报错
安全写法:
lua
const a = [[]];
[3, 2, 1].forEach(console.log)
3)以 /
(正则)开头:被当除号
bash
let x = 1, g = { test: () => 0 }, b = 1
/(a)/g.test('abc')
// 可能解析为:x = 1, g = { ... }, b = 1/ (a) / g.test('abc')
安全写法:
bash
let x = 1, g = { test: () => 0 }, b = 1;
(/(a)/g).test('abc')
4)以 `````(模板字符串)开头:与上一行粘连执行
javascript
const f = () => ''
const g = f
`Template`.match(/(a)/)
// 可能被视作:g `Template`(...); 发生意外求值
安全写法:
javascript
const g = f;
`Template`.match(/(a)/)
五、真实代码里的"名场面"
名场面 1:a
、b
、c
自增
css
let a = 1, b = 1, c = 1
a
++
b
++
c
// 由于后置 ++ 的 "no LT here",ASI 会在 a 后插入分号 → a 仍为 1,b、c 变为 2
名场面 2:IIFE 必须"自带分号"
scss
;(() => { /* ... */ })()
;(() => { /* ... */ })()
// 这不是"多余分号",而是**故意**在无分号风格中,避免与上一行粘连
名场面 3:return
+ 注释/换行
scss
function f() {
return /* comment */ 1 // 注释包含换行也视为换行 → ASI 提前插号
}
f() // → undefined
六、工程化落地:风格统一 + 机械化守护
1)工具链一键固化
-
Prettier:统一格式 & 自动加/去分号
semi: true
(始终保留分号)semi: false
(移除分号,推荐配合下方 ESLint 规则)
-
ESLint(核心规则)
json{ "rules": { "semi": ["error", "never"], // 若走无分号流派 "no-unexpected-multiline": "error", // 侦测多行解析歧义 "no-unreachable": "error", "no-cond-assign": "error", "no-unsafe-negation": "error" } }
-
TypeScript:与 ESLint/Prettier 组合即可,保持同一策略。
2)编码规约(无分号风格)
- 任何以
([/
或 ````` 开头 的新行 ,上一行末显式补分号; return/throw/yield/await/async
后不要换行;- 团队 README 中保留**"危险行首清单"**,CI 叠加 ESLint + Prettier 双关口。
七、如何给团队"定调"?
写分号派(semi: true
)
- ✅ 最省心,几乎零坑点
- ✅ 混合新旧代码库/异构团队更稳
- ❌ 视觉"噪音"略多
无分号派(semi: false
)
- ✅ 代码更简洁
- ✅ 与部分社区主流风格一致(如早期 StandardJS)
- ❌ 必须牢记"危险行首四件套",并依赖工具/评审守住边界
我的建议:
- 新团队、多人协作、混合经验层次 → 写分号(稳)
- 小团队、统一工具链、对 ASI 边界心里有数 → 无分号(简)
你需要的不是"正确答案",而是全员一致的答案 + 工具保证。
八、速查清单(收藏)
- ✅ 这几处绝对不要 断行:
return / throw / yield / await / async / 标注 break/continue / 后置 ++/-- / 箭头前
- ⚠️ 无分号风格下的危险行首 :
(
、[
、/
、````` - 🛠 工具固化:Prettier(
semi
)、ESLint(no-unexpected-multiline
) - 🧪 提交前:本地/CI 统一格式化 + lint 检查
互动彩蛋
你们团队现在是写分号 还是无分号 ?有没有踩过"危险行首"的坑?
在评论区说说你的真实翻车案例,我帮你语法层面还原事故现场并给出最小规避改法 👇