但是这种花里胡哨的东西真的可以提升我们的编码效能吗?
恐怕 不是的!
炫耀技能并不一定表明技术水平高超
许多人以使用语言和框架的晦涩功能为荣,通过利用各种罕见的 API 来展示他们对框架的熟悉程度。
他们认为自己的编程和技术能力比那些写简单逻辑的同学要高。甚至还会产生 “傲慢” 的情绪。
这种观点是 非常错误 的!
我们来看下面的例子:
在招聘开发人员时,存在一个有趣的现象,即:更高级别的申请人较少审查具体的编码技能,而更多地审查架构能力、业务理解和工程质量。
请注意,理解架构、业务和工程并不是关于沟通或管理等软技能,这是工程师坚实的专业能力。
另一方面,技术社区中最受欢迎的内容通常是各种框架和库的“入门指南”。
从畅销的《XXX从初学者到专家》指南到《一步一步教你如何学习XXX》,最热门的内容仍然围绕着如何操作这些API并应用某些技术知识模式。
考虑到这些现象和一些常识,我们可以得出以下三个条件:
- 程序员的级别越高,技能树中技术知识所占的比例就越小。
- 高级程序员的比例远小于初级程序员。
- 在编程界,大多数人关心的是技术知识。
根据这些条件,我们可以做出一个宽松的推论:最关心和痴迷于编程技能的人很可能是大多数处于初级水平的程序员。 因此,炫耀技能并不一定表明技术水平高超。
需要澄清的是,我们并不认为编程技能不重要。相反,高级程序员对技能的理解比初级程序员要深刻得多,许多熟练的代码可以在数量级上优化和解决问题。
那么我们应该如何评估这样的代码呢?
能够应用各种高级功能的学生无疑被认为是“聪明”的。然而,何时以及如何使用它们需要基于所谓“智慧”的判断。
这与 Facebook 和 Google 的代码标准中经常看到的说法类似:
运用你最好的判断力。
虽然听起来很悦耳,但这是一个形式上的概念。
下面我们将进行一些更具体的讨论并提取一些常见的技巧。
常见技巧
幸福的家庭都是相似的;每个不幸的家庭都有自己的不幸。
——列夫·托尔斯泰
好的代码就像好的文学作品,坏的代码有它自己的坏处。
如果我们使用“聪明的技巧”来评估一段代码,那么这段代码的质量可能不是很好。对于使用各种技术的代码,我们还可以找到许多这些技术应用不佳的场景来表明它们各自的问题。
使用危险的语义
很多人读完《高级程序设计》之类的书后,都会将自己对高级功能的理解运用到实际项目中去炫耀。在前端领域,这些行为包括但不限于:
- 学习 == 和 === 的区别,并在不同的情况下使用不同的符号进行逻辑判断。
- 了解变量提升(var)行为后,使用它来实现特殊的代码执行序列。
- 理解 prototype 和 constructor ,并利用它们实现各种继承关系。
- 掌握 this 指向的各种规则,并使用特殊的规则来绑定上下文。
- ……
使用具有这些功能的代码当然可以运行,但这里的问题是这些语义都是 危险的,或者是语言设计问题造成的渣滓。
既然知道它们很难使用,并且有成熟的替代解决方案可用,为什么还要用它们来展示自己的技术技能呢? 然而在前端社区,这样的行为却屡屡发生。
例如:
- 仅了解各种规则this指向就足以写一篇长文章(这在许多技术社区中一直是一个无聊的话题)。
- 令人惊讶的是,像“==”这样有无数陷阱的功能,很多人在阅读博客后竟然想要 “合理” 地使用它们。
- 至于变量提升,这种完全违反直觉的设计缺陷已被一些人用来创建各种花哨的面试问题。
当然,这决不妨碍理解这些所谓的“高级功能”如何工作以及为什么它们会导致令人困惑的行为。
对于每个想要成长的可靠学生来说,学习它们很重要。这里给出的建议是:
- 至少理解一次,达到能够指出问题所在的程度。
- 了解这些功能的替代解决方案,并了解如何避免陷阱。
- 除非维护底层库,否则在任何情况下都不要在代码中使用它们。
每种编程语言在其开发过程中都不可避免地留下遗留问题。对于JS这样一个一周诞生、向前兼容性要求非常高的语言来说,这个问题就更加严重了。
然而,随着软件工程的发展,这些设计缺陷带来的危险语义已经慢慢淡入历史。现在,像深入研究IE6兼容性问题一样,深入学习、掌握和使用它们已经逐渐过时了。
应用设计模式
设计模式也是技术文章中非常常见的主题。
例如,很多文章都把《设计模式》中的几十种模式应用到了 JavaScript 中,用上面提到的各种“高级特性”来模拟这个、实现那个。最后,他们还夸夸其谈,这些模式都是“优秀程序员必知”,所以在简历上加上一句“精通各种设计模式”,就让人印象深刻了!
设计模式的初衷是为了弥补Java等静态语言的缺点。随着编程语言的发展,许多“经典”设计模式已经成为语言机制的一部分。
比如:export 内置对单例模式的支持,用函数层包装内容就是工厂模式,yield 也实现了迭代器模式等等。
此外,JS 的动态性使得 JSON 的灵活性大幅超越了反射,而一等公民的函数设计使得 JS 回调函数比 Java 的回调接口或类似 Visitor 的模式更加灵活。
很多提倡设计模式的文章之所以没有传播开来,是因为它们人为地制造了不必要的复杂性,反而造成了一种误解:“如果你不使用XX模式,就意味着你的技能缺乏”。
至少从我个人阅读优秀的开源项目源代码来看,我还没有发现机械应用模式的实例;相反,需要解决的问题被清楚地描述,然后提供可读的抽象。
当然,我们可以回顾性地识别其中的某些实施模式;然而,我更愿意相信作者并没有以“这里我们需要使用XX模式”的心态来编码。
许多缺乏经验、缺乏洞察力的初学者可能会因为缺乏阅读高质量代码的经验或受到公司遗留项目中旧代码的影响而最终遵循严格规定的方法。在我看来,这是相当遗憾的。
减少代码行数
我们都知道,通过复制粘贴生成的冗长且重复的代码是不好的。
然而,大多数复制粘贴发生在期限紧迫且没有时间进行优化的情况下。考虑到我们的工作强度,这是可以理解的。
另一方面,还有另一种极端行为,其目的是通过使用各种非常规手段来“简化”代码,从而达到“最简洁”的代码。
例如:
- 刚开始学习函数式编程的同学可能对这样的代码情有独钟 a(b(c(d, e(f, g)))),认为函数的深度嵌套可以大大减少中间变量,从而节省代码量;
- 有些学生喜欢用逻辑运算符连接各种条件逻辑,并将它们全部写在一行中,例如 a || b && c && d;
- 同样常见的情况是,实用程序函数最终会写入越来越多的参数,直到它们在一行中一次性传递。
我们再考虑一下这样的代码是否增强了可读性:
- 深度嵌套的函数调用会带来大量的右括号,例如:)))))),这在 Lisp 中长期以来一直受到批评;
- 单行条件逻辑不利于调试;
- 具有许多参数的函数往往表现复杂且难以调试。
这些编码实践可以轻松地替换为具有更好可读性的表单,而无需太多麻烦。然而,故意创建这样的代码可能会让后续的维护者感到不舒服。
对于有关换行和缩进的具体实践,JavaScript 标准样式等工具可以有效地自动处理大多数情况。
隐式重写常识
现代工程框架通常提供许多可定制的接口,允许开发人员轻松修改框架的行为。
例如:React 公开了上下文,而 Redux 和 MobX 等库则利用此接口来极大地优化深度prop传递的体验。
然而,框架内,存在许多隐含的约定和规范,当在典型业务代码中进行不合理的定制时,可能会导致重大挑战。这些类型的修改通常发生在不显眼的地方,但可能会产生很大的影响。
例如:在我们之前维护的一个项目中,进行了巧妙的修改,React.Component用基类自己的XXX.BaseComponent行为替换了基类。
定制的组件并没有涉及任何业务逻辑相关的改动,只是增加了一些莫名其妙的初始化代码。
结果,有关 React 组件 Base 的隐含常识变得无效。维护时,乍一看,替换的组件似乎很普通。但是,修复就会导致问题。
此外,这些“黑魔法”代码没有评论或文档,也不清楚为什么首先引入它们是为了解决什么问题。对于这样的编码做法,也许除了过于聪明之外,没有其他合理的评价。
该项目的另一个例子涉及另一种“聪明”的做法:
window.fetch 根据请求路径将其替换为三到四个不同的自定义版本。这意味着当维护人员编写新的获取请求时,他们不能依赖任何先前有关获取的隐式知识,而是必须通过跟踪前辈的自定义版本来进行调试!
还有一些隐性做法会因副作用而出现问题。例如:当看到 user = getUser(id) 时,人们可能不会想到这个getUser函数不仅查询用户,而且还会默默地显示一条提示消息发送请求,然后还清除当前数据。
当然,在前端开发本身中,管理大量与 UI 相关的网络副作用本质上会增加复杂性。
然而,如果调用一个函数会导致许多级联结果,导致复杂性进一步增加,许多维护者可能会选择弃用和重写。
重新发明轮子
在技术社区中,经常可以看到“最全的前端实用功能”之类的合集,而且点赞数往往很高。然而,500合1的小霸王游戏卡比超级马里奥更好玩吗?
我有幸读过一些这样的文章,发现这些封装的函数往往甚至没有固定的主题:左边是“ getCookie”,右边是“ deepClone”,上面是isEmail,下面是scrollTop。每个实现在将英文函数名翻译成中文的层面上只有几行注释,没有测试用例、依赖配置或文档。它们被称为“小而美”。
这样的代码值得复制到你的项目中以供重用吗?坦白说,它们只是满足“我能发明轮子”冲动的产品。
当然,我完全相信作者可以毫不费力地写出优雅的内容。但项目不是采访;而是项目。对于一个稳定可靠的库,除了简单的实现之外,还需要很多与代码无关的东西。
根据《人月神话》中的布鲁克斯定律,软件项目中实际编码时间只占1/6;剩余的大部分时间用于测试、文档和沟通。对于具有更高质量要求的库代码——从在线源匆忙编写或复制(哦不!让我们称之为内联)代码是否足够?
在正式项目中使用库时,如果稳定的现有依赖项能够令人满意地满足要求,那么显然这应该是您的首选。如果你遇到需要自己重新发明轮子的情况,那么请确保在可靠项目留下的 5/6 之外编写代码也做得很好;不要不必要地重复劣质轮子。