在技术社区中,经常可以看到一些博客推广编程语言的各种“高级特性”和“高级模式”,并使用这些特性编写一些“优雅”的代码。那么,学习和使用这些东西到底是好是坏呢?本文旨在帮助你做出判断。
技巧的巧妙与智慧的智慧
许多人以使用语言和框架的晦涩特性为荣,通过利用各种罕见的 API 展示自己对框架的熟悉度。他们认为自己的编程和技术水平高于那些编写简单逻辑的学生。这个观点是否合理?下面通过几个例子来推导一下:
首先,在招聘开发人员时,有一个有趣的现象:层次越高的应聘者,考察的具体编码技能越少,而更多考察架构能力、业务理解和工程质量。请注意,理解架构、业务和工程不是指沟通或管理等软技能,而是工程师的扎实专业能力。
另一方面,在技术社区中,最受欢迎的内容往往是各种框架和库的“入门指南”。从畅销的《XXX 从初学者到专家》指南到《一步一步教你如何学习 XXX》,最热门的内容仍然围绕如何操作这些 API 和应用某些技术知识模式。
结合这些现象和一些常识,我们可以得出以下三个条件:
- 程序员的层次越高,其技能树中技术知识的比例越小。
- 高级程序员的比例远小于初级程序员。
- 在编程社区中,大多数人关心的是技术知识。
根据这些条件,我们可以做出一个松散的推论:那些最关注和痴迷于编程技能的人,很可能是初级程序员中的大多数。这样,炫耀技巧不一定能表明技术水平高超。
需要明确的是,我们并不认为编程技能不重要。相反,高级程序员对技能的理解远比初级程序员深刻,许多高超的代码可以在数量级上优化和解决问题。那么,我们应该如何评价这样的代码呢?
能够应用各种高级特性的学生无疑被认为是“聪明”的。然而,何时何地使用它们需要基于所谓的“智慧”来判断。这类似于 Facebook 和 Google 的代码标准中经常看到的说法:
用你最好的判断力。
虽然听起来令人愉快,但这是一个非常玄妙的概念。下面我们将进行一些更具体的讨论,并提取一些常见的技巧和技术。
常见的技巧和技术
使用危险的语义
许多人在阅读了《高级程序设计》之类的书后,会将他们对高级特性的理解应用到实际项目中,以炫耀自己的技术水平。在前端领域,这些行为包括但不限于:
- 了解 == 和 === 的区别,并在不同情况下使用不同的符号进行逻辑判断。
- 了解变量提升行为,并利用它实现特殊的代码执行顺序。
- 理解原型和构造函数,并使用它们实现各种继承关系。
- 掌握 this 指向的各种规则,并利用特殊规则绑定上下文。
- ...
虽然使用这些特性的代码确实可以运行,但问题在于这些语义都是危险的,或者是语言设计问题造成的糟粕。在已知它们难以使用并且有成熟替代解决方案的情况下,为什么要使用它们来炫耀自己的技术水平?然而在前端社区中,这种行为屡见不鲜。例如,仅仅理解 this
指针的各种规则就足以写一篇长文(这在许多技术社区中早已成为一个乏味的话题)。而像 ==
这样的特性虽然有无数的陷阱,令人惊讶的是仍有许多人在阅读博客后“合理”地使用它们。至于变量提升,这种完全反直觉的设计缺陷被一些人用来创造各种花哨的面试题。
当然,这绝不是反对理解这些所谓的“高级特性”如何工作以及为什么它们会导致混乱的行为。对于每一个想要成长的可靠学生来说,学习它们是很重要的。这里给出的建议是:
- 至少了解它们一次,达到能够指出问题所在的水平。
- 学习这些特性的替代解决方案,并知道如何避免陷阱。
- 除非维护底层库,否则不要在代码中使用它们。
应用设计模式
设计模式也是技术文章中非常常见的一个话题。例如,许多文章将《设计模式》中的几十种模式应用到 JavaScript 中,使用上述的各种“高级特性”模拟和实现这些模式。最后,他们会提升说这些模式都是“优秀程序员必须知道的”,因此在简历上加上一行“熟练掌握各种设计模式”显得很有吸引力!
设计模式的最初目的是弥补静态语言如 Java 的不足。随着编程语言的发展,许多“经典”设计模式已经成为语言机制的一部分。例如,export 对单例模式的内置支持,用函数层包裹内容就是工厂模式,yield 也实现了迭代器模式等等。此外,JS 的动态特性使得 JSON 的灵活性远远超过 reflection,而函数作为一等公民的设计使得 JS 的回调函数比 Java 的回调接口或 Visitor 模式灵活得多。
许多提倡设计模式的文章并不是因为它们人为地制造了不必要的复杂性,而是因为它们创造了一种误解,即“如果不使用 XX 模式,说明你的技能不足”。至少从个人阅读优秀开源项目源码的经验来看,我没有发现机械地应用模式的实例;相反,问题得到了清晰的描述,随后提供了可读的抽象。当然,可以事后在其中找到某些实现的模式;然而,我更愿意相信作者在编码时并没有“这里需要使用 XX 模式”的心态。然而,许多缺乏辨别能力的初学者可能会因为缺乏阅读高质量代码的经验或受到公司遗留项目中旧代码的影响,最终固执地遵循这些刻板的方法。在我看来,这相当遗憾。
减少代码行数
我们都知道,通过复制粘贴生成的冗长和重复的代码是不好的。然而,大多数复制粘贴发生在时间紧迫、没有时间进行优化的情况下。考虑到我们的同学们的工作强度,这可以理解。另一方面,还有另一种极端行为,即通过各种非常规手段“简化”代码,以实现“最简洁”的代码。
例如,刚开始学习函数式编程的学生可能会特别喜欢 a(b(c(d, e(f, g)))) 这样的代码,认为深度嵌套函数可以大大减少中间变量,从而节省代码量;有些学生喜欢用逻辑运算符连接各种条件逻辑,并将它们全部写在一行,如 a || b && c && d常见的还有实用函数的参数越来越多,直到一次性传入一行。
再考虑一下,这样的代码是否增强了可读性。深度嵌套的函数调用带来了许多右括号,如 )))))), 这在 Lisp 中早已备受诟病;单行条件逻辑不利于调试;具有许多参数的函数往往表现复杂,难以调试。
这些编码习惯可以很容易地以更好可读性的形式替换,而不会带来太多麻烦。然而,故意创建这样的代码可能会使后续的维护人员感到不适。对于行间断和缩进的具体实践,工具如 JavaScript Standard Style 可以自动处理大多数情况。
隐式重写常识
现代工程框架通常提供许多可定制的接口,允许开发人员轻松修改框架的行为。例如,React 公开了上下文,而 Redux 和 MobX 等库利用这个接口大大优化了深度 prop 传递的体验。然而,在典型业务代码中,框架内的许多隐含约定和规范,如果不合理地进行定制,会导致重大挑战。这些类型的修改通常发生在不起眼的地方,但可能有很大的影响。
例如,在我们之前维护的一个项目中,有一个巧妙的修改,用自己的 XXX.BaseComponent 替换了 React.Component 基类。自定义组件没有涉及任何与业务逻辑相关的更改,而是添加了一些无法解释的初始化代码。结果是关于 React 组件基类的隐含常识失效了。在维护时,乍一看替换的组件看起来很普通;然而,回滚会导致问题。此外,这些“黑魔法”代码没有注释或文档,不清楚最初引入它们是为了什么问题。对于这种编码实践,除了过于聪明之外,似乎没有其他合理的评价。
另一个例子是这个项目中的另一个“聪明”做法,即根据请求路径用三四个不同的自定义版本替换 window.fetch(不用担心——不会告诉你为什么!)。这意味着当维护人员编写新的 fetch 请求时,不能依赖之前关于 fetch 的任何隐含知识,而必须通过追踪前任的自定义版本进行调试——这很神奇,不是吗?
还有一些隐式实践,问题出在副作用上。例如,当看到 user = getUser(id) 时,可能不会期望这个 getUser 函数不仅查询用户,还悄悄显示提示消息、发送请求并清除当前数据。当然,在前端开发中,管理众多与 UI 相关的网络副作用本身增加了复杂性。然而,如果调用一个函数会导致许多连锁结果,进一步增加复杂性,许多维护人员可能会选择弃用和重写。
重新发明轮子
在技术社区中,你经常可以看到“最全面的前端实用函数”之类的合集,并且它们的点赞数往往很高。然而,一个 500 合 1 的小霸王游戏卡比超级马里奥更有趣吗?
我有幸读过一些这样的文章,发现这些打包的函数往往连固定主题都没有:左边是 getCookie,右边是 deepClone,上面是 isEmail,下面是 scrollTop。每个实现只有几行相当于将英文函数名翻译成中文的注释,没有测试用例、依赖配置或文档。它们被称为“小而美”。
这样的代码值得复制到你的项目中重复使用吗?坦率地说,它们只是满足“我可以发明轮子”冲动的产品。当然,我完全相信作者可以轻松优雅地编写一个深拷贝函数。但是项目不是面试;对于一个稳定可靠的轮子库,除了简单的实现外,还需要许多与代码无关的东西。根据布鲁克斯定律(《人月神话》)的软件项目中,实际编码时间只占 1/6;其余的大部分时间需要用于测试、文档和沟通。对于具有更高质量要求的库代码,匆忙编写或从网上复制(哦不!让我们称之为内联)代码能否满足?
在正式项目中使用库,如果稳定的现有依赖满足需求,显然应首先选择。如果遇到需要自己重新发明轮子的情况,确保在可靠项目的5/6
之外的时间也做好;不要不必要地重复劣质轮子。
追求高层次的抽象
最后一点可能更为小众,因为对于许多人来说,复制粘贴即可满足需求,这违反了他们的习惯。然而,正因为如此,这是一种更高级形式的“巧妙”。
[高级] 听起来像是纯粹的圣杯。[高阶函数] 和 [高阶组件] 似乎是 [高级程序员] 的完美搭配。然而,如果你需要维护这些高阶函数,你会怎么想?
() => () => () => () => 123
一个返回 123 的函数,由另一个函数返回,确实很高层次。但是这会不会让你感到困惑...
结语
在编程中,技巧和智慧是不可或缺的,但仅仅依赖技巧并不能保证代码质量。通过理解和运用合适的设计模式、提高代码可读性以及避免不必要的复杂性,我们可以编写出既高效又易于维护的代码。这不仅是对技术能力的证明,也是对团队合作和项目成功的承诺。