摘要:最近在清理的未讀列表,看到了才知道了的,一種自動插入分號的機(jī)制。這種行為被叫做自動插入分號,簡稱。不過在省略分號的風(fēng)格中,這種解析特性會導(dǎo)致一些意外情況。規(guī)則標(biāo)準(zhǔn)定義的包括三條規(guī)則和兩條例外。規(guī)則一情況三就是為量身定做的。
TL;DR
最近在清理 Pocket 的未讀列表,看到了 An Open Letter to JavaScript Leaders Regarding Semicolons 才知道了 JavaScript 的 ASI,一種自動插入分號的機(jī)制。因?yàn)槲沂?“省略分號風(fēng)格” 的支持者,之前也碰到過一次因?yàn)楹雎苑痔柈a(chǎn)生的問題,所以對此比較重視,也特意多看了幾份文檔,但越看心里越模糊。并不是我記不住 ( 和 [ 前面記得加 ; 這種結(jié)論,而是覺得看過的幾篇文章跟 ECMAScript 標(biāo)準(zhǔn)描述的有點(diǎn)區(qū)別。直到最近反復(fù)琢磨才突然有了 “原來如此” 的想法,于是就有了此文。
這篇文章會用 ECMAScript 標(biāo)準(zhǔn)的 ASI 定義來解釋它到底是如何運(yùn)作的,我會盡量用平易近人的方法描述它,避免官方文檔的晦澀。希望你跟我一樣有收獲。掌握 ASI 并不能夠讓你馬上解決手頭的問題,但能讓你成為一個更好的 JavaScript 程序員。
什么是 ASI按照 ECMAScript 標(biāo)準(zhǔn),一些 特定語句(statement) 必須以分號結(jié)尾。分號代表這段語句的終止。但是有時候?yàn)榱朔奖?,這些分號是有可以省略的。這種情況下解釋器會自己判斷語句該在哪里終止。這種行為被叫做 “自動插入分號”,簡稱 ASI (Automatic Semicolon Insertion) 。實(shí)際上分號并沒有真的被插入,這只是個便于解釋的形象說法。
這些特定的語句有:
空語句
let
const
import
export
變量賦值
表達(dá)式
debugger
continue
break
return
throw
下面這段是我 個人的理解,上的定義同時也表示:
所有這些語句中的分號都是可以省略的。
除此之外其他的語句有兩種情況,一是不需要分號的(比如 if 和函數(shù)定義),二是分號不能省略的(比如 for),稍后會詳細(xì)介紹。
那么 ASI 如何知道在哪里插入分號呢?它會按照一些規(guī)則去判斷。但在說規(guī)則之前,我們先了解一下 JS 是如何解析代碼的。
Token解析器在解析代碼時,會把代碼分成很多 token 。一個 token 相當(dāng)于一小段有特定意義的語法片段??匆粋€例子你就會明白:
var a = 12;
上面這段代碼可以分成四個 token :
var 關(guān)鍵字
a 標(biāo)識符
= 運(yùn)算符
12 數(shù)字
除此之外,(,. 等都算 token ,這里只是讓你有個大概的概念,比如 12 整個是一個 token ,而不是 1 和 2。字符串同理。
解釋器在解析語句時會一個一個讀入 token 嘗試構(gòu)成一個完整的語句 (statement),直到碰到特定情況(比如語法規(guī)定的終止)才會認(rèn)為這個語句結(jié)束了。記得上文提到的 變量賦值 這個語句必須以分號結(jié)尾么?這個例子中的終止符就是分號。用 token 構(gòu)成語句的過程類似于正則里的貪婪匹配,解釋器總是試圖用盡可能多的 token 構(gòu)成語句。
接下來是重點(diǎn):任意 token 之間都可以插入一個或多個換行符 (Line Terminator) ,這完全不影響 JS 的解析,所以上面的代碼可以寫成下面這樣(功能等價):
var a = // = 和 12 之間有兩個換行符 12 ;
這個特性可以讓開發(fā)者通過增加代碼的可讀性,更靈活地組織語言風(fēng)格。我們平時寫的跨多行的數(shù)組,字符串拼接,和鏈?zhǔn)秸{(diào)用都屬于這一類。不過在省略分號的風(fēng)格中,這種解析特性會導(dǎo)致一些意外情況。
比如這個例子中,以 / 開頭的正則會被理解成除法:
var a , b = 12 , hi = 2 , g = {exec: function() { return 3 }} a = b /hi/g.exec("hi") console.log(a) // 打印出 2, 因?yàn)榇a會被解析成: // a = b / hi / g.exec("hi"); // a = 12 / 2 / 3
事實(shí)上這并不是省略分號的風(fēng)格的錯誤,而是開發(fā)者沒有理解 JS 解釋器的工作原理。如果你傾向省略分號的風(fēng)格,那了解 ASI 是必修課。
ASI 規(guī)則ECMAScript 標(biāo)準(zhǔn)定義的 ASI 包括 三條規(guī)則 和 兩條例外。
三條規(guī)則是描述何時該自動插入分號:
解析器從左往右解析代碼(讀入 token),當(dāng)碰到一個不能構(gòu)成合法語句的 token 時,它會在以下幾種情況中在該 token 之前插入分號,此時這個不合群的 token 被稱為 offending token :
如果這個 token 跟上一個 token 之間有至少一個換行。
如果這個 token 是 }。
如果 前一個 token 是 ),它會試圖把前面的 token 理解成 do...while 語句并插入分號。
當(dāng)解析到文件末尾發(fā)現(xiàn)語法還是有問題,就會在文件末尾插入分號。
當(dāng)解析時碰到 restricted production 的語法(比如 return),并且在 restricted production 規(guī)定的 [no LineTerminator here] 的地方發(fā)現(xiàn)換行,那么換行的地方就會被插入分號。
兩條例外表示,就算符合上述規(guī)則,如果分號會被解析成下面的樣子,它也不能被自動插入:
分號不能被解析成空語句。
分號不能被解析成 for 語句頭部的兩個分號之一。
你會發(fā)現(xiàn)這些規(guī)則相當(dāng)晦澀,好像存心考你智商的,還有些坑爹的專有名詞。不要緊,我們來看幾個非常簡單的例子,看完之后你就會明白所有這些東西的含義。
例子解析 第一個例子:換行a b
我們模擬一下解析器的思考過程,大概是這樣的:解析器一個個讀取 token ,但讀到第二個 token b 時它就發(fā)現(xiàn)沒法構(gòu)成合法的語句,然后它發(fā)現(xiàn) b 和前面是有換行的,于是按照規(guī)則一(情況一),它在 b 之前插入分號變成 a ;b,這樣語句就合法了。然后繼續(xù)處理,這時讀到文件末了,b 還是不能構(gòu)成合法的語句,這時候按照規(guī)則二,它在末尾插入分號,結(jié)束。最終結(jié)果是:
a ;b;第二個例子:大括號
{ a } b
解析器仍然一個個讀取 token ,讀到 token } 時發(fā)現(xiàn) { a } 是不合法的,因?yàn)?a 是表達(dá)式,它必須以分號結(jié)尾。但當(dāng)前 token 是 },所以按照規(guī)則一(情況二),它在 } 前面插入分號變成 { a ;},這句就通過了,然后繼續(xù)處理,按照規(guī)則二給 b 加上分號,結(jié)束。最終結(jié)果是:
{ a ;} b;
順帶一提,也許有人會覺得 { a; }; 這樣才更自然。但 {...} 屬于塊語句,而按照定義塊語句是不需要分號結(jié)尾的,不管是不是在一行。因?yàn)閴K語句也被用在其他地方(比如函數(shù)定義),所以下面這種代碼也是完全合法的,不需要任何分號:
function a() {} function b() {}第三個例子:do while
這個是為了解釋規(guī)則一(情況三),這是最繞的部分,代碼如下:
do a; while(b) c
這個例子中解析到 token c 的時候就不對了。這里面既沒有換行也沒有 },但 c 前面是 ),所以解析器把之前的 token 組成一個語句,并判斷該語句是不是 do...while,結(jié)果正好是的!于是插入分號變成 do a; while(b) ;,最后給 c 加上分號,結(jié)束。最終結(jié)果為:
do a; while (b) ; c;
簡單點(diǎn)說,do...while 后面的分號是會自動插入的。但如果其他以 ) 結(jié)尾的情況就不行了。規(guī)則一(情況三)就是為 do...while 量身定做的。
第四個例子:returnreturn a
你一定知道 return 和返回值之間不能換行,因?yàn)樯厦娲a會解析成:
return; a;
但為什么不能換行?因?yàn)?return 語句就是一個 restricted production。這是什么意思?它是一組有嚴(yán)格限定的語法的統(tǒng)稱,這些語法都是在某個地方不能換行的,不能換行的地方會被標(biāo)注 [no LineTerminator here]。
比如 ECMAScript 的 return 語法定義如下:
return [no LineTerminator here] Expression ;
這表示 return 跟表達(dá)式之間是不允許換行的(但后面的表達(dá)式內(nèi)部可以換行)。如果這個地方恰好有換行,ASI 就會自動插入分號,這就是規(guī)則三的含義。
剛才我們說了 restricted production 是一組語法的統(tǒng)稱,它一共包含下面幾個語法:
后綴的 ++ 和 --
return
continue
break
throw
ES6 箭頭函數(shù)(參數(shù)和箭頭之間不能換行)
yield
這些不用死記,因?yàn)榘凑粘R?guī)書寫習(xí)慣,幾乎沒人會這樣換行的。順帶一提,continue 和 break 后面是可以接 label 的。但這不在本文討論范圍內(nèi),有興趣可以自己探索。
第五個例子:后綴表達(dá)式a ++ b
解析器讀到 token ++ 時發(fā)現(xiàn)語句不合法,因?yàn)楹缶Y表達(dá)式是不允許換行的,換句話說,換行的都不是后綴表達(dá)式。所以它只能按照規(guī)則一(情況一)在 ++ 前面加上分號來結(jié)束語句 a,然后繼續(xù)執(zhí)行,因?yàn)榍熬Y表達(dá)式并不是 restricted production ,所以 ++ 和 b 可以組成一條語句,然后按照規(guī)則二在末尾加上分號。最終結(jié)果為:
a ;++ b;第六個例子:空語句
if (a) else b
解釋器解析到 token else 時發(fā)現(xiàn)不合法,本來按照規(guī)則一(情況一),它在應(yīng)該加上分號變成 if (a) ;,但這樣 ; 就變成空語句了,所以按照例外一,這個分號不能加。程序在 else 處拋異常結(jié)束。Node.js 的運(yùn)行結(jié)果:
else b ^^^^ SyntaxError: Unexpected token else第七個例子:for
for (a; b )
解析器讀到 token ) 時發(fā)現(xiàn)不合法,本來換行可以自動插入分號,但按照例外二,不能為 for 頭部自動插入分號,于是程序在 ) 處拋異常結(jié)束。Node.js 運(yùn)行結(jié)果如下:
) ^ SyntaxError: Unexpected token )如何手動測試 ASI
我們很難有辦法去測試 ASI 是不是如預(yù)期那樣工作的,只能看到代碼最終執(zhí)行結(jié)果是對是錯。ASI 也沒有手動打開或關(guān)掉去對比結(jié)果。但我們可以通過對比解析器生成的 tree 是否一致來判斷 ASI 加的分號是不是跟我們預(yù)期的一致。這點(diǎn)可以用 Esprima 在線解析器 完成。
拿這段代碼舉例子:
do a; while(b) c
Esprima 解析的 Syntax 如下所示(不需要看懂,記住大概樣子就行):
{ "type": "Program", "body": [ { "type": "DoWhileStatement", "body": { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "a" } }, "test": { "type": "Identifier", "name": "b" } }, { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "c" } } ], "sourceType": "script" }
然后我們把加上分號的版本輸入進(jìn)去:
do a; while(b); c;
你會發(fā)現(xiàn)生成的 Syntax 是一致的。這說明解釋器對這兩段代碼解析過程是一致的,我們并沒有加入任何多余的分號。
然后試試這個有多余分號的版本:
do a; while(b); c;; // 結(jié)尾多一個分號
Esprima 結(jié)果:
{ "type": "Program", "body": [ { "type": "DoWhileStatement", "body": { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "a" } }, "test": { "type": "Identifier", "name": "b" } }, { "type": "ExpressionStatement", "expression": { "type": "Identifier", "name": "c" } }, { // 多出來一個空語句 "type": "EmptyStatement" } ], "sourceType": "script" }
你會發(fā)現(xiàn)多出來一條空語句,那么這個分號就是多余的。
結(jié)尾如果看到這里,相信你對 ASI 和 JS 的解析機(jī)制已經(jīng)有所了解。也許你會想 “那我再也不省略分號了”,那我建議你看看參考資料里的鏈接。而且就我的經(jīng)驗(yàn),即使是分號的堅(jiān)持者,少數(shù)地方也會無意識地使用 ASI 。比如有時候忘了寫分號,或者寫迭代器中的單行函數(shù)時。下次我會說下對省略分號的風(fēng)格的看法,和如何用 ESLint 保證代碼風(fēng)格的一致性。
參考資料ECMAScript: ASI
ECMAScript 標(biāo)準(zhǔn)定義。本文的概念和很多例子完全遵照它來寫的。但也強(qiáng)烈建議你自己看看。
JavaScript Semicolon Insertion Everything you need to know
關(guān)于 ASI 的解釋,略微學(xué)術(shù)化,講得很詳細(xì),也很客觀。
An Open Letter to JavaScript Leaders Regarding Semicolons
NPM 作者對 ASI 和兩種風(fēng)格的看法,這篇更注重個人觀點(diǎn)的表達(dá)。他是省略分號風(fēng)格的傾向者。
Esprima: Parser
一個在線 JS 解析器。你可以輸入一些語句來看看 token 都是什么。也可以通過 Tree 的變化來測試加不加分號的影響。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/87716.html
摘要:行結(jié)束符之后的符號有二義性,使得該符號與上條語句能夠無縫對接,不導(dǎo)致語法錯誤。然而在中,有幾種特殊語句是不允許行結(jié)束符存在的。如果語句中有行結(jié)束符,會優(yōu)先認(rèn)為行結(jié)束符表示的是語句的結(jié)束,這在標(biāo)準(zhǔn)中稱為限制產(chǎn)生式。 showImg(https://segmentfault.com/img/bVmyZB); 什么是 ASI ? 自動分號插入 (automatic semicolon i...
摘要:中分號自動插入轉(zhuǎn)譯自鏈接描述在中,分號自動插入機(jī)制允許在一行代碼結(jié)尾省略分號。比如分號自動插入規(guī)則分號插入只是一個術(shù)語。如果在這些位置遇到換行了,分號將被插入。 JavaScript中分號自動插入 轉(zhuǎn)譯自:鏈接描述在JavaScript中,分號自動插入機(jī)制允許在一行代碼結(jié)尾省略分號。你應(yīng)該養(yǎng)成一直書寫分號的習(xí)慣,與此同時掌握J(rèn)avaScript分號省略處理機(jī)制是十分重要的。因?yàn)檫@不僅有...
摘要:這段代碼工作正常,盡管沒有用分號在某些場景下是很管用的,特別是,有時候可以幫助減少代碼錯誤。比如不好的寫法盡管這段代碼能正常工作,但代碼中我們應(yīng)盡量避免使用。前言 在我們平時工作中寫代碼是最頻繁的事情了,但我們的代碼真的好看嗎? 預(yù)計(jì)本文閱讀時間(10分鐘) 正文 1.1--語句結(jié)尾 我們來看一段代碼 //合法的代碼 var name = Dreams; function sayName(...
摘要:自動填補(bǔ)分號的規(guī)則在說要不要寫分號之前,先了解一下自動填補(bǔ)分號的規(guī)則。后來看到知乎上的作者尤雨溪和前端大神賀師俊的回答后,我對寫分號的想法完全顛覆了??偸菍懛痔柌⒉荒芡耆鉀Q缺陷如后換行會自動插入分號。 在打算寫這篇文章之前,我是一個分號黨,在寫這篇文章之后,可能會轉(zhuǎn)為無分號黨了。之前是寫分號是編輯器語法較檢所養(yǎng)成的強(qiáng)迫癥,現(xiàn)在觀念的轉(zhuǎn)變,是因?yàn)榭戳瞬簧俅笊竦挠懻摵?,覺得javascr...
摘要:規(guī)范理論標(biāo)準(zhǔn)定義了自動分號插入規(guī)則,包括以下三個基本規(guī)則加兩個前置條件前置條件如果插入分號后解析結(jié)果是空語句,那么不會自動插入分號。 規(guī)范理論 es5 標(biāo)準(zhǔn)定義了自動分號插入規(guī)則,包括以下三個基本規(guī)則加兩個前置條件: 前置條件 1、如果插入分號后解析結(jié)果是空語句,那么不會自動插入分號。 例子:(空語句,else 前不加分好) if (a > b) else c = d 2、如果插入分號...
閱讀 2518·2023-04-25 22:09
閱讀 1031·2021-11-17 17:01
閱讀 1572·2021-09-04 16:45
閱讀 2626·2021-08-03 14:02
閱讀 822·2019-08-29 17:11
閱讀 3259·2019-08-29 12:23
閱讀 1094·2019-08-29 11:10
閱讀 3284·2019-08-26 13:48