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