别再拿奇技淫巧搬砖了

28,668 阅读14分钟

在技术社区中,经常会看见一些博客鼓吹编程语言的各种【高级特性】与【进阶模式】,并且给出一些使用这些特性的【优雅】代码。那么学习和使用这些东西是好是坏,有何利弊呢?本文旨在帮助你给出自己的判断。

技巧的小聪明与大智慧

有不少人以使用语言、框架的冷门特性为荣,通过使用各种生僻的 API 来展示自己对框架的熟悉,进而认为自己的编程能力和技术水平比起编写朴素逻辑的同学要高。这个观点合理吗?下面,我们用几个例子来给出一个推论:

首先,在招聘开发同学时,有一个很有趣的现象,即对于越高层级的应聘者,对具体编码技巧的考察越少,而对架构能力、业务理解、工程素质的考察越多。请注意,对架构、业务与工程的理解绝非通用性的沟通、管理等【软素质】,而是作为工程师实打实的专业能力。

另一方面,在技术社区中最火热的内容,往往是各种框架、类库的【入门指南】。从各种爆款的【XXX 从入门到精通】到【手把手教你学 XXX】,最热门的内容的仍然是这些 API 怎么调、模式怎么套一类的技巧性知识。

综合这些现象以及一些常识,我们可以得到下面这三个条件:

  • 越高级的程序员,技巧性知识在能力树内的比重越小。
  • 高级程序员的比重远小于初级程序员。
  • 程序员社区中的多数,最关注的恰好是技巧性知识。

根据这几个条件,我们可以给出一个不严谨的推论:**最关注并醉心于编程技巧的,很可能恰恰是广大处于初级水平的程序员。**这样一来,炫耀技巧的行为也就不能表明技术水平的高超了。

需要澄清的是,这里我们绝对不是认为编程技巧不重要。相反,高级程序员对技巧的熟悉显然远超初级同学,而许多技巧性的代码更能够在数量级上优化并解决问题。那么我们该怎么评价这样的代码呢?

会运用各种高级特性的同学,毫无疑问是【聪明】的。但在什么场合使用、怎样使用,则需要所谓【智慧】的判断。这就好像 Facebook 和 Google 的代码规范里时常出现的这句话:

Use your best judgement.

虽然好听,但这其实一个非常形而上的概念。下面我们会做一些更具体化的讨论,抽离出一些常见的奇技淫巧。

常见的奇技淫巧

幸福的家庭都是相似的,不幸的家庭各有各的不幸。

——托尔斯泰

好的代码都是相似的,烂的代码各有各的烂。

如果我们用【奇技淫巧】来评价某段代码,那么这段代码的质量多半好不到哪里去。对于使用了各种技巧的代码,我们也能够找到不少【把技巧运用得很糟糕】的场景,来指明它们各自的问题所在。

使用危险语义

不少人在读了些【XX 高级程序设计】之类的书以后,会为了炫耀自己对【高级特性】的理解,而将它们运用到实际项目中。在前端范畴内,这些行为包括但不限于:

  • 学会 ===== 的区别后,在不同场合使用不同的符号实现判断逻辑。
  • 学会 变量提升 的行为后,使用它来实现特殊的代码执行顺序。
  • 学会 prototypeconstructor 后,使用它们实现各种继承关系。
  • 学会 this 的各种指向规则后,使用特殊的规则来绑定上下文。
  • ……

使用这些特性的代码当然能够运行,但这里的问题在于这些语义都是危险的,或是语言的设计问题造成的糟粕。**在明知它们难以使用,并且早有成熟替代方案的情况下,为什么要使用它们来显示自己的技术水平呢?**偏偏在前端社区,这样的行为又是此起彼伏。比如,光是搞懂各种 this 的指向规则,就足够写一篇长文(在许多技术社区这早已经是日经的无聊文章了)。而 == 这种罄竹难书的特性,居然也有很多人在读了博客【深入掌握】后拿来【合理使用】。至于 变量提升 这样完全反直觉的设计缺陷,都有的是人拿来编排出各种花哨的面试题。

当然,这里绝不是反对去了解这些所谓的【高级特性】如何工作,以及它们为什么会产生令人困惑的行为。对每一位希望成长的靠谱同学,学习它们都是很重要的。这里给出的建议是:

  1. 至少学懂它们一次,达到能够指出它的问题在哪的程度。
  2. 学会这些特性的替代方案,知道如何避免踩坑。
  3. 除非维护底层基础库,否则坚决不在代码中使用它们。

每一门编程语言,在自己的发展过程中都难免留下遗留的问题。对于 JS 这种一个周末诞生,前向兼容要求又非常之高的语言来说,这个问题更是严重。但随着软件工程的发展,这些设计缺陷带来的危险语义已经慢慢淡出历史,而在现在,深入学习、掌握和使用它们,和深入 IE6 兼容性问题一样已经逐渐过时了。而如果有人用这些问题来刁难你,你大可以这么怼回去:

对,我知道 this 有四种绑定方式,我还知道回字有四种写法呢。

套用设计模式

总的来说,如果光从字面上讲,程序里确实是有一些“模式”可以发掘的。因为你总是可以借鉴以前的经验,用来构造新的程序。你可以把这种经验叫做“模式”。可是自从《设计模式》(通常叫做 GoF,“Gang of Four”,“四人帮”)这本书在 1994 年发表以来,“设计模式”这个词有了新的,扭曲的含义。它变成了一种教条,带来了公司里程序的严重复杂化以及效率低下。

——《解密 “设计模式”》

设计模式也是一个非常泛滥的技术文章主题。比如,有不少文章将《设计模式》里十几种模式都搬到了 JS 上,再用上面提到的各种【高级特性】来模拟这个、实现那个,最后升华一句说这些模式都是【优秀的程序员必须掌握】的,这样在简历上加一行【掌握各种设计模式】简直逼格满满啊!

设计模式的产生初衷,是为了补充 Java 这样的静态语言的不足。许多【经典】设计模式,在编程语言的演化中,早已成为语言机制的一部分。比如,export 内建了对单例模式的支持、将内容用 function 包装一层就是工厂模式、yield 也实现了迭代器模式等等。再比如,JS 的动态性使得 JSON 的灵活性大大超越了反射,而函数一等公民的设计也使得 JS 的回调函数比 Java 的回调接口或 Visitor 等模式灵活得多。

许多鼓吹设计模式的文章,其流毒并不在于它们人为制造了不必要的复杂度,而是造成了一种【不用 XX 模式就说明你的水平不行】的错觉。至少,在个人阅读过的优秀开源项目源码中,并没有发现生搬硬套模式的地方,而是描述清楚代码要解决的问题,而后给出易读的抽象即可。你当然可以事后总结出它们实现了某些模式,但我更愿意相信作者不是按照【这里要使用 XX 模式】的思维方式来编码的。然而,对许多缺乏鉴别能力的新手同学,如果没有阅读高质量代码的经验,在公司历史项目老代码的【熏陶】下,也可能走上套模式的八股之路。私以为这是很可惜的。

紧缩代码行数

我们都知道,复制粘贴得到的冗长重复代码是糟糕的。但是,复制粘贴多数发生在工期紧张,来不及优化的场景下。按照天朝同学们的工作强度,这也是可以理解的。但与之相对地,是另一种矫枉过正的行为,即为了实现代码的【最简】,用各种匪夷所思的手段去【精简】代码。

比如,刚刚入门函数式编程的同学,可能会对 a(b(c(d, e(f, g)))) 这样的代码情有独钟,认为通过深度嵌套的函数,能够大大减少中间变量,进而节约代码量;再比如,一些同学喜欢把各种判断逻辑用逻辑运算符连接起来,一鼓作气地串成 a || b && c && d 这样的判断逻辑在一行中写完;还有,对于工具函数,也很常见参数越写越多,然后【一口气一行传完所有参数】的情形。

我们不妨再考虑一下,这样的代码可读性真的更高吗?深层嵌套的函数调用会带来大量形如 )))))) 的右括号,这在 Lisp 中被诟病已久;单行的判断逻辑不利于调试;传入大量参数的函数行为往往趋于复杂而不易调试(想想高考时区区 f(x, y) 就能玩出多少花样,f(a, b, c, d, e) 的变量排列组合起来可以有多么复杂)。

这些编码实践其实都很容易无痛地替换为可读性更强的形式,也并非什么大问题。不过,刻意去制造这样的代码多少就会和后面的维护者过意不去了。对于具体的换行、缩进实践,用形如 JavaScript Standard Style 这样的工具就能够自动化地处理掉绝大多数的情形了。

隐式改写常识

Explicit is better than implicit.

——《The Zen of Python》

现代的工程框架一般都提供了许多定制用的接口,开发者可以轻松地通过这些接口改写框架的行为。比如,React 开放了 context,Redux 和 MobX 一类的库利用这个接口,大大优化了 props 深度传递的体验。不过,对框架有许多约定俗成的隐式【共识】,在一般的业务代码中被不合理地定制时,就会造成很大的困扰了。这类改写通常发生在不起眼的地方,但其影响范围往往反而很大。

比如,在我们维护过的某个项目中,出现过一种将 React.Component 基础类【巧妙地】改写,替换为自己的 XXX.BaseComponent 的行为。定制后的组件并没有什么和业务逻辑相关的改动,而是加入了一些莫名其妙的初始化代码。这样一来,原本对 React 组件基类隐式的【通用常识】就失效了。在维护时,替换后的组件初看之下并没有什么特别之处,但一旦替换回去就会造成问题。并且,这些黑科技代码既没有注释也没有文档,更不知道加入它们的初衷是为了解决什么问题。对于这样的编码实践,恐怕除了自作聪明以外也没有什么更合理的评价了。

再比如,这个项目里有另一种【巧妙的】实践,会将 window.fetch 根据请求的路径,替换为三四种不同的定制版本(放心吧,不会告诉你为什么要这么做的!)。这样,在维护者编写新的 fetch 的时候,就不能够运用任何之前对 fetch 隐式的已有知识,而是必须追踪到前人的定制版本里去调试了,是不是很神奇呢?

还有一些隐式的实践,其问题在于【副作用】。比如,在看到一句 user = getUser(id) 的时候,你恐怕不会希望这个 getUser 函数不光帮你查询了用户,还默默地帮你弹了个提示、发了个请求、再清空了当前数据吧?当然,在前端领域内,其本身的一部分复杂度就在于对大量 UI、网络等副作用的管理。不过如果调用一个函数会造成许多【牵一发而动全身】的结果,许多维护者可能会选择弃用重写,从而造成复杂度的进一步增加。

当然,相信有不少厂商还是会非常喜欢带隐式副作用的代码的。比如,隐式安装全家桶的 360

重复发明轮子

在技术社区也经常能看到形如【最全常用前端工具函数】的合集,并且它们的点赞数量往往也很高。不过,500 合一的小霸王游戏卡,真的比超级玛丽好玩吗?

作者有幸拜读过一些这样的文章,发现这些打包赠送的函数,常常甚至连固定的主题都没有:左一个 getCookie,右一个 deepClone,上一个 isEmail,下一个 scrollTop。而其中的每个实现,都只有区区几行【把英文函数名翻译成中文】级别的注释,没有测试用例、依赖配置和文档说明,美其名曰【小而美】。

这样的代码值得复制到你的项目中复用吗?不客气地说,它们只是满足【我会造轮子】性冲动的产物而已。个人当然充分相信作者有能力随手写出一个优雅的深拷贝,但项目不是面试,对于一个稳定可信赖的轮子,在简单实现之外,更需要不少与代码无关的内容。按照《人月神话》的经验,软件项目中实际编码的时间只占 1/6,而剩余的时间更需要测试、文档、沟通。对于对质量要求更高的库代码,信手编写或从网上复制粘贴(哦不,说好听点叫内联)的代码真的堪用吗?

在正式的项目中使用类库时,稳定的已有依赖若能满足要求,那么显然是第一选择。如果遇上了需要自己动手造轮子的地方,那就尽心把代码之外,一个靠谱项目剩下的 5/6 也做好吧,不要去重复发明质量低劣的轮子。

追求高阶抽象

最后这一点恐怕比较小众,因为对不少只靠复制粘贴就能够实现需求的人而言,这显然和他们的习惯是相违背的。不过,正因为如此,它才是一种更加进阶的【奇技淫巧】。

【高阶】听起来就是个纯净的圣杯。【高阶函数】和【高阶组件】看上去更是【高端程序员】的绝配。不过,如果需要维护这样的高阶函数,你会怎么想呢?

() => () => () => () => 123

一个 返回返回返回返回 123 的函数的函数的函数的函数,确实很高阶,但真的不会把你绕晕吗…

总结

不少同学如果一路读完(忍受)到了这里,可能会吐槽【这些技巧在复杂啊高级啊什么的场景下很重要啊 blahblah】,故而在最后澄清几点:

  1. 对于多数影响维护性的技巧,在日常编写的业务逻辑里不仅不需出现,还应尽量避免。尤其对于【这里刚好能用上昨天学的那个 API】这类的动机,更需要三思。
  2. 在正经的大型开源项目中,不可避免地存在不少技巧性 Hack。这时候你很可能会发现相应的地方会有不少注释不厌其烦地告诉你为什么这么写,这是非常值得借鉴学习的。
  3. 这里并不是反对去掌握高阶的编程技巧,而是希望不要将一些【自作聪明】的代码草率地引入正式项目中,给后面的维护带来不便。
  4. 如果希望抛开业务项目的沉重包袱,去学习一下如何【正确地运用奇技淫巧】,参与开源项目或许是一条捷径。对于这一点,希望本文的前作 零起点的开源社区贡献指南 能够有所帮助。

最后对于可能的质疑,作者个人的 Github 里有些简单的玩具轮子,吐槽请不要客气…😅