JavaScript語句後應該加分號麼?

[size=medium]這是一個老生常談的問題了。我之前就曾經寫過[url=http://hax.iteye.com/blog/382186]一篇blog[/url]記錄了我對此問題的實踐與思考之旅。最近在知乎上又出現了這方面的爭論,而且幾乎是一面倒的支持“總是寫分號”。這讓我深深覺得是時候正本清源,祛除迷信了。於是我在問題[url]http://www.zhihu.com/question/20298345[/url]下,花了整整一天時間寫了以下的回答。

重新發在blog上,主要是因爲此文過長,作爲知乎的答案或許應該精簡一下,但全文內容乃心血結晶,值得留存,照錄如下。


首先,加還是不加,這是一個書寫風格問題。而書寫風格通常有一些外在的考量,比如團隊所建立的規則或習慣。@玉伯 的[url=http://www.zhihu.com/question/20298345/answer/14666757]答案[/url]就是基於此。我對此基本贊同,不過這其實有點避重就輕,呵呵。另外,即使團隊有這樣的規則,也未必要通過強制在寫代碼的時候就要這樣寫,而可以通過工具達成。比如在源碼管理工具上掛上鉤子,對提交的源代碼自動整理格式。

其次,[url=http://www.zhihu.com/question/20298345/answer/14665666]很多人提到代碼壓縮問題[/url]。我覺得這是[b]非常扯淡[/b]的理由。如果2012年的今天一個JS壓縮器還不能正確處理分號,這隻能說明這個JS壓縮器沒有達到基本的質量要求,根本不值得信任。

@馮超 和 @CSS魔法 [url=http://www.zhihu.com/question/20298345/answer/14667309]提到的jslint[/url]也是一個工具的反面例子。工具是幫助人的,而不應該是[b]強迫[/b]人的。不明白這一點,你就不會理解爲什麼在已經有jslint很多年的情況下,還會出現jshint。

jshint對於不寫分號會報warn,但可以通過asi選項關閉(在文件頭加上/* jshint asi:true */即可)。

在asi選項說明裏,[url=http://www.jshint.com/options/]jshint的文檔[/url]是這樣寫的:
[quote]There is a lot of FUD (fear, uncertainty and doubt) spread about semicolon spreaded by quite a few people in the community. The common myths are that semicolons are required all the time (they are not) and that they are unreliable. JavaScript has rules about semicolons which are followed by [b]all[/b] browsers so it is up to you to decide whether you should or should not use semicolons in your code.[/quote]

翻譯如下(【】裏是我添加的說明):

[quote]關於分號有大量的FUD,且是由社區裏的一小撮人【你知道是指誰】散佈的。一個常見的流言是必須寫分號,不寫分號不可靠【流言的意思是不寫分號會導致代碼行爲不確定】。實際上JS有明確的分號規則,並且[b]所有[/b]瀏覽器【居然】都忠實遵守了規則。所以是否應該在你的代碼裏使用分號,完全可以由你自己決定【而不是由一小撮流言散佈者或二逼工具強加於你】。[/quote]

所以對於可不可以不加分號這個問題,社區是有結論的。

然後所謂“應該不應該”,就只是利弊分析,而不是非黑即白。其中也必定有一些如“可維護性”、“可理解性”甚至“代碼美感”之類的貌似“賤人賤智”的問題。不過我相信有經驗的程序員還是會在大多數問題上找到共識的。


這個世界上有許多語言。大量語言是不用分號作爲EOS(End of Statement)的。有些偏執狂認爲不用分號的語言都是垃圾,對此我沒啥好說的。

有些語言也是可選分號,比如python。python是可以加分號作爲語句結束的。當然絕大多數python程序員是不會加分號的(除了在一行裏寫多個語句)。所以python和js一樣是可選分號!並且python的習慣是不寫分號(僅在極少數情況下寫)!

也有不少人會指摘python的語法太特殊,比如縮進啥的……不能算是c-style的。不過即使是C風格的語言,也有不寫分號的,比如groovy。groovy和js一樣是可選分號!並且groovy的習慣是不寫分號(僅在極少數情況下寫)!

所以至少從同樣兩個是可選分號的語言來看,不寫分號在實踐上是可行的。畢竟,既然被設計爲可選,那麼合理的推斷是:語言的設計初衷是傾向於鼓勵不寫分號。

實際上,不少人(包括我)認爲,c-style的分號[b]本來就是多餘[/b]的。爲什麼這麼說?因爲明確的EOS只是給編譯器的提示而已。如果漏了分號,編譯器會報錯。既然它都報錯了,顯然它知道這裏應該有EOS。既然它知道,那麼幹嘛還要我寫?

給編譯器以hint,這在幾十年前是一個平衡編譯器和用戶成本的設計。某些語言(如Fortran、Basic等)選擇用換行來作爲EOS,這樣每行只能一個語句,並且一個語句折行必須用特殊的接續符號。某些語言(如C)則選擇了通過分號來達成,這樣每行可以多個語句,並且一個語句也可以分佈在多行。平心而論,我更喜歡前一種策略。不過現實是c-style的語法流傳更廣,至少當前的工業主流語言都是c-style的。

在c-style語言中,如果既要允許自由折行,又要避免額外的EOS(分號),編譯器會較爲複雜,光靠看token是不能確定語句是否結束的(即換行處有可能是語句結束,也有可能不是)——儘管在實踐中只需要很少的規則,人就能一目瞭然的看清語句是否結束,但是parser要處理一切的極端情況,例如在換行前插入註釋到底怎麼算。而C的設計是遵循所謂worse is better的哲學,非常強調[b]實現簡單[/b],一個明確的EOS對於編譯器來說絕對是簡單的。當初如果有人找K&R去要求應該由編譯器判斷這裏該不該是語句結束,我打包票肯定被K&R扁死。有趣的是,lisp那一幫人更極端,如果你抱怨括號實在太密密麻麻的了,一定有人語重心長的告訴你S表達式纔是王道。

其實像C++編譯器也已經複雜到超乎想象,按理說可選分號真是小事一樁,但它因爲要保持對C的完全兼容,所以還是必須寫分號。

python和groovy的parser則都是有名的複雜。這並不完全由允許分號可選造成,但是可選的分號其實是整個語法設計哲學的一環。如Groovy的哲學是PHIM——Parse how I mean。

話說python的語法設計真的非常有意思。它也有問題,比如tab和空格混合,計算機之子@程劭非 曾經驚歎,居然有語言能通過改變註釋(註釋中可定義tabsize)就改變了語義和行爲,真是極品。

當然後來者會吸取教訓,比如coffeescript和jade之類的,也都是依賴縮進,但是都不允許tab和空格混用。

所以tab/sp這是python的坑。Guido Van Rossum現在就後悔了。從某種程度上說,JavaScript的分號就有點類似python的tab/sp問題。

正如混合tab/sp是出自GVR的良好初衷(讓你們想用啥就用啥),可選分號也是出自BE的良好初衷(隨便你寫不寫)。也如同tab/sp一樣,良好的初衷並不代表就沒有隱患。之所以python、groovy就沒有可選分號的爭議,而js就有爭議,其實正說明js存在一些問題。

其實Groovy歷史上也是有關於可選分號爭議的,參見:[url]http://blog.csdn.net/hax/article/details/139490[/url]。不幸的的是,與Groovy早期經過社區激烈的討論纔得到穩定語法不同,JS是一門早熟的語言,一些早期的設計失誤沒有機會被修復。自動分號插入算法就是其中之一。總體上,自動分號插入算法還算正常,但是在一些小地方留下了不易發覺的坑。比如return語句。
[/size]


return
{
a:1
}

[size=medium]在return後會自動插入分號,導致完全違背期望的結果。

這一古怪行爲往往被解釋爲在JS中應採用一行內跟隨大括號的書寫風格(即Java的風格,或者說是K&R的C的原初風格,而不是C#風格),其實追根述源,問題還是出在分號上。

不要插分號的地方被插了分號,這挺坑爹了,但更更坑爹的是想要插的結果沒插。這就是括號的問題。如果下一行的開始是“(”、“[”上一行的結尾不會被加上“;”。

如:[/size]
a = b
(function(){
...
})()

[size=medium]
會被解釋爲[/size]
a = b(function(){...})()



[size=medium]其實如果我們真想表達上述代碼,通常會這樣寫:[/size]
a = b(function(){
...
})()

[size=medium]再如:[/size]
a = b
[1,2,3].forEach(function(e){
console.log(e)
})


[size=medium]實際效果等價於[/size]
a = b[3].forEach(function(e){
console.log(e)
})


[size=medium]坑爹的是,搞不好這代碼說不定還能運行!你要事後通過調試發現這些錯誤是相當滴痛苦啊。

當然這也不能全賴BE。在JS的早期,還沒有數組迭代方法 Array.prototype.forEach/map/filter...等,也沒有今天常見的 (function(){...})() 慣用法,所以這個問題其實很不明顯。但是到了今天,這些坑爹的問題就都冒出來了。

實際上,“+”、“-”、“/”也有問題,但是我們幾乎不會在實踐中遇到。因爲你幾乎不可能會寫出行首以“+”、“-”、“/”開始的語句,除了 ++i 之類的語句(但是其實我們都會寫成 i++)。

不過這些問題的解決方案其實也很簡單。只要在“[”、“(”、“+”、“-”、“/”等之前加分號就可以了:[/size]
a = b
;(function(){
...
})()

a = b
;[1,2,3].forEach(function(e){
console.log(e)
})


[size=medium]
有些同學覺得這樣很醜。沒問題,你可以用 void 替代“;”。

也有不少人覺得[url=http://www.zhihu.com/question/20298345/answer/14662711]這是一種“不一致”[/url],需要記住額外的法則。

我承認採取這樣一種方法你必須記住一些特例。但是幾乎所有的語言都有一些歷史原因導致的坑,並且JS也不止這一個坑。更關鍵的是,即使你採用了總是寫“;”的方法,仍然不能避免掉進EOS的坑,因爲造成問題的asi特性仍然存在。比如之前提到的return後面會自動插分號的問題。

“總是寫分號”,相比“不寫分號但是edge case要在行首加分號”,[b]看上去[/b]要更“簡單”,但這只是描述簡單,[b]實際做起來[/b]未必更簡單。

比如你必須要記得,function表達式後面也要寫“;”!

如:[/size]
function a() {
...
}
[1,2,3].forEach(...)

[size=medium]
這代碼是沒問題的,但是你改成[/size]
var a = function () {
...
}
[1,2,3].forEach(...)

[size=medium]
就有問題了!這坑爹!

對於“始終加分號派”來說,結果就會變成函數後面也一定要加分號。(你分得清函數聲明和函數表達式嗎?坑爹啊,不如都加!)但是爲什麼函數就加而 if ... {} 或 for (...) {...} 結構裏的大括號後面就不加分號呢?這不是也不一致嘛。

而且,同樣是一條特殊規則,[b]行首加分號的規則比函數表達式後面加分號的規則其實要簡單[/b]![/size]

var a = function () {
...
}
[1,2,3].forEach(...)


[size=medium]還是以上面代碼爲例。

行首是否要加分號,我只要看[b]本行的第一個字符[/b]就可以了。因爲對於object[prop]這樣的意圖,其實沒有程序員會寫出[/size]
object
[prop]

[size=medium]這樣的代碼。如果他要折行,一定是寫成[/size]
object[
prop
]

[size=medium]所以行首第一個字符如果是括號,毋庸置疑的,這一定是一個新語句的開始。

反過來,你如果要判斷“}”後面是否要加“;”,你得向上回溯,看清楚整段代碼是一個結構呢?還是一個函數?如果是函數的話,是函數聲明呢?還是函數表達式!

許多時候,你可能向上翻幾頁還沒找到對應的“{”!或者已經忘記了是幾層縮進了!

由此可見,對於人來說,行首特例加分號的策略其實更簡單易行。而總是加分號的策略聽上去簡單,執行起來卻難!除非你的策略最後變成了所有“}”之後都加分號——我真見過有人這麼做的。


對人是這樣,下面再來看看對機器(引入工具)的情形。特別的,因爲有不少人表示他遵循總是寫分號的方式是因爲他嚴重依賴jslint。所以我就拿jslint開刀。

對於總是加分號的策略,你希望工具能提示你哪裏缺少分號。但是實際情況是,你必須儘量避免寫出有歧義的跨行語句,因爲工具很難判斷是有意爲之,還是忘記寫“;”。

比如:[/size]
a = b
(function(){
...
})();

[size=medium]這代碼在jslint的提示是:Expected '(' at column 5, not column 1.

請問你是應該真的按照它的提示把括號移動到b後面嗎??

仔細考慮一下,你就知道這個問題不好回答。因爲jslint給出的建議其實是基於“這是合法的代碼,只是格式不妥”。雖然我們都知道這[b]更可能[/b]是忘記寫分號。

再來一個更坑爹的例子:[/size]

/*jslint white: true */
var a,b,c,d,e,f,g,h,i,j,k,l,m,o,s;
a=b+c*d-e
/f/g-h*i/j
/f/g.exec(s).map(f);


[size=medium]這段代碼在jslint裏是[b]不報錯[/b]的!!!

但是我們是可以看出來這代碼很有可能是缺少分號。

這裏可以看出,如果排除了whitespace的格式提示(這事兒還是挺常見的,畢竟許多人不喜歡被強制加那麼多空格規則),jslint其實無法在我們最需要幫助的時候幫到我們!因爲它無法判斷這個地方到底是有意爲之(不用“;”而跨行),還是忘記寫“;”。

反過來說,如果採取行首特例加“;”的習慣,其實工具是很容易判斷你是否忘記加了分號。如果加上一些對縮進信息的判斷來排除極少數不良的折行習慣(出warning即可),工具甚至能自動把所有這類分號都加上。


兩種策略:

1. 我總是寫分號,讓工具告訴我哪裏我忘記寫了(但是有時候可能還報不出來,或報了個其他信息)

2. 我總是不寫分號,讓工具自動把(由於語言設計缺陷所要求的)必須的分號加上去

哪種更好?


總結:

我所推薦的不寫分號的方式,其實不僅是不寫分號,而是同時採用更嚴格的跨行策略,即只允許在當前行處於未完成狀態時跨行(就像你在jsshell中輸入代碼一樣)。這條規則其實並不需要特別強制,因爲絕大多數程序員一直就是這樣在執行。誠然,存在少數人習慣寫這樣有歧義的折行代碼:[/size]

a = b + c
+ d + e
+ f + g


[size=medium]但是這個習慣不難糾正,並且工具根據縮進等信息是完全能檢測到的。


說到這裏,也許有些同志認爲這隻能說明jslint太挫,不能證明到處寫“;”的風格不好。因爲工具也可以同時加上其他限制嘛。不過你仔細想想,可以發現這是一個悖論。如果jslint夠智能,引入了其他與分號無關的代碼風格要求,比如空格和縮進,還有折行風格,確實也可以更精確的找到所有漏掉分號的地方。但是那無非再次證明了一點:[b]編譯器(代碼分析器)完全可以知道哪裏應該有EOS[/b]。既然所有的分號其實可以由機器自行加上(無論是加在行首還是行尾),那麼我們自己還要手寫所有分號的意義到底在哪裏?!

以上。[/size]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章