读书人

JavaScript话语后应该加分号么

发布时间: 2012-06-26 10:04:13 作者: rapoo

JavaScript语句后应该加分号么?
这是一个老生常谈的问题了。我之前就曾经写过一篇blog记录了我对此问题的实践与思考之旅。最近在知乎上又出现了这方面的争论,而且几乎是一面倒的支持“总是写分号”。这让我深深觉得是时候正本清源,祛除迷信了。于是我在问题http://www.zhihu.com/question/20298345下,花了整整一天时间写了以下的回答。

重新发在blog上,主要是因为此文过长,作为知乎的答案或许应该精简一下,但全文内容乃心血结晶,值得留存,照录如下。


首先,加还是不加,这是一个书写风格问题。而书写风格通常有一些外在的考量,比如团队所建立的规则或习惯。@玉伯 的答案就是基于此。我对此基本赞同,不过这其实有点避重就轻,呵呵。另外,即使团队有这样的规则,也未必要通过强制在写代码的时候就要这样写,而可以通过工具达成。比如在源码管理工具上挂上钩子,对提交的源代码自动整理格式。

其次,很多人提到代码压缩问题。我觉得这是非常扯淡的理由。如果2012年的今天一个JS压缩器还不能正确处理分号,这只能说明这个JS压缩器没有达到基本的质量要求,根本不值得信任。

@冯超 和 @CSS魔法 提到的jslint也是一个工具的反面例子。工具是帮助人的,而不应该是强迫人的。不明白这一点,你就不会理解为什么在已经有jslint很多年的情况下,还会出现jshint。

jshint对于不写分号会报warn,但可以通过asi选项关闭(在文件头加上/* jshint asi:true */即可)。

在asi选项说明里,jshint的文档是这样写的:
return{ a:1}
在return后会自动插入分号,导致完全违背期望的结果。

这一古怪行为往往被解释为在JS中应采用一行内跟随大括号的书写风格(即Java的风格,或者说是K&R的C的原初风格,而不是C#风格),其实追根述源,问题还是出在分号上。

不要插分号的地方被插了分号,这挺坑爹了,但更更坑爹的是想要插的结果没插。这就是括号的问题。如果下一行的开始是“(”、“[”上一行的结尾不会被加上“;”。

如:

a = b(function(){  ...})()


会被解释为
a = b(function(){...})()



其实如果我们真想表达上述代码,通常会这样写:
a = b(function(){  ...})()

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


实际效果等价于
a = b[3].forEach(function(e){  console.log(e)})


坑爹的是,搞不好这代码说不定还能运行!你要事后通过调试发现这些错误是相当滴痛苦啊。

当然这也不能全赖BE。在JS的早期,还没有数组迭代方法 Array.prototype.forEach/map/filter...等,也没有今天常见的 (function(){...})() 惯用法,所以这个问题其实很不明显。但是到了今天,这些坑爹的问题就都冒出来了。

实际上,“+”、“-”、“/”也有问题,但是我们几乎不会在实践中遇到。因为你几乎不可能会写出行首以“+”、“-”、“/”开始的语句,除了 ++i 之类的语句(但是其实我们都会写成 i++)。

不过这些问题的解决方案其实也很简单。只要在“[”、“(”、“+”、“-”、“/”等之前加分号就可以了:
a = b;(function(){  ...})()a = b;[1,2,3].forEach(function(e){  console.log(e)})



有些同学觉得这样很丑。没问题,你可以用 void 替代“;”。

也有不少人觉得这是一种“不一致”,需要记住额外的法则。

我承认采取这样一种方法你必须记住一些特例。但是几乎所有的语言都有一些历史原因导致的坑,并且JS也不止这一个坑。更关键的是,即使你采用了总是写“;”的方法,仍然不能避免掉进EOS的坑,因为造成问题的asi特性仍然存在。比如之前提到的return后面会自动插分号的问题。

“总是写分号”,相比“不写分号但是edge case要在行首加分号”,看上去要更“简单”,但这只是描述简单,实际做起来未必更简单。

比如你必须要记得,function表达式后面也要写“;”!

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


这代码是没问题的,但是你改成
var a = function () {  ...}[1,2,3].forEach(...)


就有问题了!这坑爹!

对于“始终加分号派”来说,结果就会变成函数后面也一定要加分号。(你分得清函数声明和函数表达式吗?坑爹啊,不如都加!)但是为什么函数就加而 if ... {} 或 for (...) {...} 结构里的大括号后面就不加分号呢?这不是也不一致嘛。

而且,同样是一条特殊规则,行首加分号的规则比函数表达式后面加分号的规则其实要简单

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


还是以上面代码为例。

行首是否要加分号,我只要看本行的第一个字符就可以了。因为对于object[prop]这样的意图,其实没有程序员会写出
object[prop]

这样的代码。如果他要折行,一定是写成
object[ prop]

所以行首第一个字符如果是括号,毋庸置疑的,这一定是一个新语句的开始。

反过来,你如果要判断“}”后面是否要加“;”,你得向上回溯,看清楚整段代码是一个结构呢?还是一个函数?如果是函数的话,是函数声明呢?还是函数表达式!

许多时候,你可能向上翻几页还没找到对应的“{”!或者已经忘记了是几层缩进了!

由此可见,对于人来说,行首特例加分号的策略其实更简单易行。而总是加分号的策略听上去简单,执行起来却难!除非你的策略最后变成了所有“}”之后都加分号——我真见过有人这么做的。


对人是这样,下面再来看看对机器(引入工具)的情形。特别的,因为有不少人表示他遵循总是写分号的方式是因为他严重依赖jslint。所以我就拿jslint开刀。

对于总是加分号的策略,你希望工具能提示你哪里缺少分号。但是实际情况是,你必须尽量避免写出有歧义的跨行语句,因为工具很难判断是有意为之,还是忘记写“;”。

比如:
a = b(function(){...})();

这代码在jslint的提示是:Expected '(' at column 5, not column 1.

请问你是应该真的按照它的提示把括号移动到b后面吗??

仔细考虑一下,你就知道这个问题不好回答。因为jslint给出的建议其实是基于“这是合法的代码,只是格式不妥”。虽然我们都知道这更可能是忘记写分号。

再来一个更坑爹的例子:

/*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);


这段代码在jslint里是不报错的!!!

但是我们是可以看出来这代码很有可能是缺少分号。

这里可以看出,如果排除了whitespace的格式提示(这事儿还是挺常见的,毕竟许多人不喜欢被强制加那么多空格规则),jslint其实无法在我们最需要帮助的时候帮到我们!因为它无法判断这个地方到底是有意为之(不用“;”而跨行),还是忘记写“;”。

反过来说,如果采取行首特例加“;”的习惯,其实工具是很容易判断你是否忘记加了分号。如果加上一些对缩进信息的判断来排除极少数不良的折行习惯(出warning即可),工具甚至能自动把所有这类分号都加上。


两种策略:

1. 我总是写分号,让工具告诉我哪里我忘记写了(但是有时候可能还报不出来,或报了个其他信息)

2. 我总是不写分号,让工具自动把(由于语言设计缺陷所要求的)必须的分号加上去

哪种更好?


总结:

我所推荐的不写分号的方式,其实不仅是不写分号,而是同时采用更严格的跨行策略,即只允许在当前行处于未完成状态时跨行(就像你在jsshell中输入代码一样)。这条规则其实并不需要特别强制,因为绝大多数程序员一直就是这样在执行。诚然,存在少数人习惯写这样有歧义的折行代码:

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


但是这个习惯不难纠正,并且工具根据缩进等信息是完全能检测到的。


说到这里,也许有些同志认为这只能说明jslint太挫,不能证明到处写“;”的风格不好。因为工具也可以同时加上其他限制嘛。不过你仔细想想,可以发现这是一个悖论。如果jslint够智能,引入了其他与分号无关的代码风格要求,比如空格和缩进,还有折行风格,确实也可以更精确的找到所有漏掉分号的地方。但是那无非再次证明了一点:编译器(代码分析器)完全可以知道哪里应该有EOS。既然所有的分号其实可以由机器自行加上(无论是加在行首还是行尾),那么我们自己还要手写所有分号的意义到底在哪里?!

以上。







读书人网 >JavaScript

热点推荐