最近对于代码的组织和优化思考了很多,也看完了知名的“蝴蝶书”,本着思考了还是总结一下的原则,还是记录一下自己的认识方便后面嘲讽自己有多么菜

引子

作为一名菜鸟,实际开发中由于缺乏积累,总是要踩了不少的坑才能发现自己早先到底有多么蠢。于是,许久之前写的店铺系统,维护起来渐渐地发现了很多大坑其实是可以避免的。作为教训,我觉得有必要做一下笔记,也算是写个检讨,因此这里但关注点不是我代码写得到底有多烂,而是如果我下次重新写这么几万行代码的项目,我应该注意到哪些点。

几个标准

我觉得对于大部分的前端开发工程师(这里主要面向浏览器编程)而言,JS 很多特性可能在工作中永远都不会用到,我们创建一个对象时,99% 的时候不会使用构造对象法,也不会用到位操作符。因此在对这门语言的认知还不足以成为专家的时候,我觉得评价一个人的代码水平的关键点,就是 “可读性好”

可读性,是一个众说纷纭的东西,就好比 JS 到底要不要加分号,代码缩进到底是两个空格还是四个空格,很多时候是谁也不服谁,哪个都有自己有道理但方面,自然我也不想在这里过多地纠缠。

我评价代码的可读性,主要基于以下5个点:

  1. 直白易懂,没有过多奇淫技巧
  2. 遵守一般的最佳实践
  3. 团队统一的代码风格
  4. 适度使用代码预处理和模块
  5. 方便使用版本管理工具

简单易懂往往很难做到

首先阐述第一个点,直白易懂,没有过多奇淫技巧

我很少看各种类库的源码,因为我觉得太晦涩了,虽然有时也觉得写得真是精妙,但这是建立在花费几倍的时间去理解和钻研的前提下,有时还得借助搜索引擎的帮助(其实还是因为太菜了么?读不懂啊)。因此我是强烈反对把各种奇淫技巧带到实际的工作项目中的,除非你能确定这个项目从出生到死亡都只有你一个人在干。

这里举个简单的例子:

1
2
3
4
// 儿童和妇女半价!
var price = function (age, sex) {
return age < 18 ? 50 : sex === 'female' ? 50 : 100
}

其实是很简单的逻辑,就是未成年和女性可以享受半价,但由于使用了两个三元运算符,简单的逻辑反而要认真地思考以下才知道(还有运算符的优先级问题呢)

实际工作中,我也遇到了同事非常喜欢用三元运算符的,他说三元运算符看起来更简洁一些,相比 if...else 能节省很多代码

我刚开始的时候也是觉得这样真是太赞了,看起来很牛逼的写法呢。但是现在,我在思考三元运算符到底是把事情搞复杂了还是变优雅了,对比一下:

1
2
3
4
5
6
7
8
// 儿童和妇女半价!
var price = function (age, sex) {
var result = 100
if (age < 18 || sex === 'female') {
result = 50
}
return result
}

嗯,确实,代码从一行变成了5行,足足翻了5倍!但我敢说,这次一下子就知道函数的逻辑。

实际上我还发现一个地方,那就是现在基本都会使用 UglifyJS 对代码进行压缩

1
2
3
4
5
6
7
8
// 压缩混淆前
if (condition) {
doSome(true)
} else {
doSome(false)
}
// UglifyJS 压缩混淆后
condition?doSome(!0):doSome(!1)

UglifyJSif...else 语句常用对压缩算法就是使用三元运算符!也就是说你辛苦为性能也好,为节省字节也好,只不过是消费读者的脑力。

当然,我这里并不是说性能不重要,但过早但优化是万恶之源,牺牲可读性来做到所谓的简洁和高性能,是不是有点本末倒置呢?毕竟,随着硬件网络的发展,我们通过这种方式节约的带宽和性能可以说是微乎其微。在敏捷开发的时代,效率和维护迭代才是最重要的。

经典一般都经历了时间的检验

这里说的经典,其实也算是简单易懂的延续,有些技术在刚出现时可能相当复杂,但随着时代但发展,成为每个人都知道但东西但时候,这就不能算是奇淫技巧了,而是应该叫做最佳实践

我记得我第一次写切换列表时的时候,头都大了,因为有接近二十个项目,那时候因为什么也不懂,所以写出来的是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
function checkName () {
var _name = $('#form-name')
_name.on('click', function () {
// ...继续逻辑
})
}
function checkAddress () {
var _address = $('#form-address')
_address.on('click', function () {
// ...继续逻辑
})
}
// ... 一直这样验证下去

你能想象每一个按钮都绑定了一个事件处理程序,只是为了切换不同的 Ajax 请求么?当然,现在我会用事件委托上去:

1
2
3
4
5
6
7
8
function checker () {
$('form').on('click', function () {
var type = $(this).attr('data-type')
if (type === 'name') {
// 一直这样验证下去
}
})
}

正如这里使用事件委托抽象 Ajax 请求才是最合适的做法一样,很多最佳实践是需要经验的。一个设计模式即使再好,但读者不懂得这样做但深层次原理,也就无法理解这样写的原因,于是在该读者眼里,这反而成了一种 “奇淫技巧”。因此写出好代码的一个条件,那就是自身要有足够的积累,不至于连业内通用的一些技巧或者说是最佳实践都难以理解。

统一就好办

想必都知道 JS 社区最热门的两个问题是,到底要不要加分号,缩进到底是两个空格还是四个空格好

很多这些争吵不会有一个统一的结果,因为很多时候是一个人的喜好问题而已。但当把个人但喜好带到团队中的时候,问题就开始麻烦起来了

在早先的日子里,让我去维护前人写的那些项目的时候,我很痛苦的一件事情就是,到底要不要手动帮它们的格式改成我喜欢的格式啊?最可恶的时候可能是明明是同一个文件,居然有三四种杂交的代码风格!

其实这里问题很好解决,那就是使用统一的团队代码风格,创业公司刚开始可能没有注意到这一点,但要知道,人员是会流动的,如果没有统一的团队风格指导,往往接手的人写出来的代码风格是完全不同的。这里建议的一点是,最好是使用社区的已经整理好的代码风格(实际上就是参照大公司),配合 Linter 工具有神奇的效果喔

最好一开始考虑成长的代价

这里真的是血的教训了,当你的代码长度有上万行的时候,你就会发现,很多时候时间都浪费在寻找特定的代码行。

在工作中有一个项目是实现一个店铺系统,JavaScript 代码两万行,CSS 代码近一万行的规模。可怜的我们为了能够让客户们方便地自定义代码(这根本就是一个伪需求嘛!),直接使用的全局单例模式,然后把所有模块的逻辑和样式分别塞到一个 JS 文件和 CSS 文件中……这意味着单个 JS 文件就约一万行了(桌面浏览器和手机浏览器分开实现)

写的时候当然是没什么太大问题,但是项目上线了,总会有新的需求和新的优化,这时就成为了我们的噩梦了……

首先当然是因为模块们全都塞在一起了,公有的方法控件什么的也共处一室,修改的时候我就得打开编辑器不断地跳来跳去,只因为逻辑太多
其次是对于 CSS 而言,没有用预处理的情况下,公有私有的方法混在一起非常难办,有时微调一个模块的功能,为了不影响其他模块的样式,难免引入脏代码覆盖,久而久之,我就不知道这几千条样式中是有多少条是重复的了。

因此,我在这里吸取到的教训是,如果你的 JS 代码或 CSS 有可能超过三千行,那么请严肃考虑到底要不要使用预处理器和模块化开发。如果答案是否,请再考虑该项目后续迭代的速度和功能变更可能性。

也要考虑复制粘贴

这里说一个有趣的例子,我们在写店铺系统的时候,因为是要把 DOM 结构全部写到 JS 中方便统一热更新,这里我们发现,拼接字符串真是一个好痛苦的事情,看下面几种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 写法一:好直白的一行,可是维护起来好痛苦,一行代码中貌似有一整个宇宙
var string1 = '<div class="wrapper"><div class="inner">里面还有好多嵌套的说</div></div>'
// 写法二:逼格满满的提示这里只是换行,但 IE8 不支持啊
var string2 = '<div class="wrapper">\
<div class="inner">里面还有好多嵌套的说</div>\
</div>'
// 写法三:古老的数组写法,看着确实还勉强,老大说 IE6 以前都是这么写的,性能好
var string3 = ['<div class="wrapper">',
' <div class="inner">里面还有好多嵌套的说</div>',
'</div>'].join('')
// 写法四:加号拼接,还不错的样子
var string4 = ''+
'<div class="wrapper">'+
' <div class="inner">里面还有好多嵌套的说</div>'+
'</div>'
// 这里本来留给 ES6 的字符串模版,然而都用上 ES6 还讨论这些做啥

我们最后选择了第四种写法,其实只考虑了两点:既有HTML风格的缩进,又不至于每一行风格差异太大导致经常复制粘贴错误。

是的,复制粘贴也是一个需要考虑的地方,很多人兴许没有注意到

我们来看一段代码:

1
var a, b = 1, c = null, d;

没错,就是很多有经验的人喜欢使用的一种模式,社区中又叫做单 var 模式,出现这种模式的原因么,就是因为 JavaScript 中的变量会提升,那么有人就建议在函数的开头定义好全部的变量,由于只使用来一个 var 可以很突出地告诉读者这里声明了所有的变量

乍一看好像没什么问题,不过我要指出其严重的两点不足之处,首先是对于 Git 等版本管理工具而言,如果修改了其中一个变量的初始值,那么 Git 中记录的是这一整行有变动,却又无法一眼看到到底是哪个变动了(需要整行进行对比),不过这个问题可以通过一种改进的写法来优化:

1
2
3
4
var a,
b = 1,
c = null,
d;

这样以来就解决了 Git 中变更的问题,但其实不管是优化前还是优化后,我们都要注意到一个点,就是复制粘贴代码的时候极其容易忘记把 var 声明写完整。变量全部提升到头部一起声明的一个缺点就是,后续复制用到的代码块到其他地方的时候,需要重新考虑是否把声明落下

其实我是推荐一声明一变量,用到时再定义的做法的:

1
2
3
4
5
6
// 这样既方便复制易懂,又能在 Git 中良好处理,同时也是同步的逻辑
var arr = []
var arr2 = [1, 2]
arr.push(1)
var len = arr.length
arr[len + 1] = 2

有趣的是,UglifyJS 同样会帮您优化这里,多个 var 声明的方式最后也被改成了单 var 的形式以节省字符。鉴于此,我建议是还是以可读性可维护性优先

支撑我这种思路的还有一个点是,在 ES6 中,letconst 是不存在变量提升的,同时也推荐使用时再声明。我这种做法只是学习了未来的一个最佳实践罢了

最后

每个人眼中的优雅的代码的模样可能都不同,也许有很多标准,我所能做的就是多踩些坑,能给后面会接手自己代码的人少些麻烦,也是程序员的一种美德