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