阅读 10727

给 ES6 class 说句公道话

最近知乎上有个探讨「ES6 为什么要加入 class?它是否是鸡肋?」的问题,这篇文章是对它的回答。这问题本身让我感到有些唏嘘,当初身为 ES6 最重磅新特性的 class 才几年就沦落到了路人眼中鸡肋的地步,前端社区的总路线动摇得实在太快了……

不可否认,近年来的 class 受到了很多批评。但既然要追本溯源,我们就不能忽略这么一个大前提:JS 中的 class 从根源上就是残疾的!最早 Brendan Eich 设计 JS 的时候,Netscape 管理层为了避免和 Java 的冲突,明确要求不能有 class。这项政治命令导致最早 JS 的对象模型基于更简易的原型,只算「基于对象」而非「面向对象」,无法达到 Java 这种程度的 OOP。

可是我们这里问的明明是 ES6,为什么要提到这段老黄历呢?其实这就是「ES6 为什么要加入 class」这个问题的直接答案——ES6 的 class,其目标就是完成前人未竞的事业呀!class 不是 ES5 之前从没有人想要,ES6 突然拍脑袋加上的东西。它是十几年来社区始终有很大呼声的重要特性,几经周折才终于在 ES6 里成功落地了。

让我们一句话总结下 ES6 以前的 ES12345 都在干嘛吧:

你看,实际上直到 2009 年的 ES5,JavaScript 的对象模型相比起 1995 年,都没有发生实质性的演化。我们都说 ES5 本质上是 ES3.1,我觉得你叫它 ES1.3 都可以……而现在跟 Rust 一起成为网红技术的 Trait,要到 2003 年的论文里才提出(Traits: Composable units of behavior)。鉴于 class 已经在 Java、C++、Python、Ruby 甚至 PHP 这些常见语言里获得了广泛的运用,JS 社区想引入 class 替代掉原型,从而更好地支持大规模项目开发,是很自然的。谁会喜欢「寄生混合继承」这种费解的东西呢?这就是 ES6 以前那个年代的类型体操吧。

关于 class 长期以来的重要性,摘录一段《JavaScript 20 年》中的原文:

在 2008 年 7 月发起 Harmony(即 ES6 前身)工作的 TC39 会议上,相当多时间都用来讨论「是否应该以及如何」纳入类。在 ES4 的前后两次尝试中,为了开发复杂的类定义语法和语义,人们都付出了巨大的努力……

对于 class 设计更具体的来龙去脉,可以参考这本书中完整记录的 ES6 class 设计历程,这应该是互联网上关于这个问题最权威的资料了。简单来说这里的区别在于:考虑到 ES4 的两次失败,TC39 已经不能也不敢按照「做正确的事」的方式来加入 class,而只能妥协选择不「Break the Web」的语法糖手法了。这虽然带来了 ES6 的成功,但也为今天对 class 的争议埋下了伏笔。

ES6 的 class 有很大的影响。现在大家普遍不待见 React 的 class 组件,可是你还记得 React 0.14 年代的 createClass API 吗?我记得至少在某个历史阶段,ES6 的 class 被普遍视为重要的、基础性的、民心所向的语言特性。现在则不然,因为咱们的总路线又摇摆了……

现在,对 JS 中 class 的批评主要集中在这些地方:

  • 基于原型模拟,越来越难继续演化。decorator 提案始终难产,private field 争议很大,TS 和 ECMAScript 标准之间也越来越难贴合。
  • 和 JSX 式的 UI 组件契合度不高,丢掉了前端框架这块大市场。三大框架里 React 基本是为了保证前向兼容性才留着 class 组件,Vue 也废弃了 class 提案,只有 Angular 还在用带类型标注(非标准)的 class。
  • 常常被作为「手里有了锤子看什么都是钉子」的某种富有仪式感的模板,往设计模式教科书里套,带来不必要的抽象封装。
  • 在接手维护大项目时,过于容易「再包一层」而导致滥用。

作为 class 的替代方案,其实我们完全可以通过朴素的函数配合对象字面量语法(也就是直接写 {a:1, b:2} 这种东西),很大程度上在业务逻辑中替换掉不必要的 class。传统上这种方式有所谓的 ad-hoc 问题,也就是显得琐碎松散,看起来是种缺乏「结构化」的构造。但 TS 的 Interface 实际上通过 Structural Typing 解决了这个问题,因此呼声很高。

不过,个人并不认为 JS 中 class 的问题能体现出「FP 终将替代 OOP」的所谓「历史趋势」,这看上去更像是相当特定于 JS 的一种现象。因为毕竟 JS 的 class 先天不足地基于原型,而且对象字面量又非常方便(从这个语法衍生出的 JSON,说不定是 JS 对整个计算机产业最大的贡献了)。如果换一门语言,就未必是这样。比如,Dart 可以用一堆 Widget 的 class 来建模出类似 JSX 函数嵌套的 UI,而 C++ 的 lambda 和 class 也完全不是替代关系。Rust 倒是有 Trait,但是你知道 ES6 的 class 设计者早就做了 Trait 提案吗?并且这个 JS 中的 Trait 居然是因为性能问题而无法落地的——难道这证明 Trait 不如 class 吗?

在现代 JS 中,class 显然有自己的价值所在:

  • class 很容易作为工厂模式的语法糖,new 出长期存在的对象实例来用。
  • 在为 JS 环境扩展各种平台基础能力的时候,class 是一种建构「有状态对象」的基本原语,很适合 1:1 地把 C++ 的 class 和 JS 的 class 映射起来,也有 N-API 的现成支持。
  • class 有良好的 IDE 支持,如果设计得当,你在开发新需求时并不需要修改散落在项目四处的细粒度函数逻辑,而是只需集中修改内聚在 class 中的代码,亦即数据和行为。
  • 最重要的是,class 代表了一种工业界开发语言中最普遍的思维模型。它就像英语一样,可以把「类、实例、生命周期」这些概念拿来和几乎所有具备正常技能水平的程序员沟通。作为反例,其实 Objective-C 老师傅更加面向对象,更加「组合优于继承」呢。语言最终要靠人去使用,小众的概念设定(比如 OC 的 protocol 和 delegate)哪怕实际上闪烁着创新的光芒,也会存在难学难招人难培育生态的问题,这是种马太效应式的社会现象。

实际上,在整个 GUI 开发的工业界,class 的建模算是个相当成功的行业案例了。GUI 中的 Element / View / Layer 这些抽象,都很容易用树形继承的方式来表达。比如对于某种 Canvas 框架,所有的 Layer 都需要有 x / y / w / h / hidden / opacity 这些状态,它们也需要一个在场景被 walk 时被访问到的 paint 方法。这时你弄个公共 BaseLayer 出来,就是个简单明了的设计。用这种思维模型,很容易读懂 Flutter Engine 在内的大量的 GUI 框架。另外,JS 之外的 class 一般使用 Nominal Type System,它们对 AOT 编译优化更加友好。从这个角度来说,Dart 的 class 设计就更接近当年 ES4 路线的完全形态,比 TS 难用些但性能上限高。

现在,这里可以回答「class 是否鸡肋」的问题了。个人认为编程是解决问题的手段,并没有必要政治正确地坚持某种范式,而是应该始终使用语言中最简单方便的构造来表达抽象——在现代前端技术栈(TS + UI 框架)的舒适区内,这个构造常常不是 class。但编程语言是复杂的,是随历史演化的。就像每个人都只会用到软件的某个 10% 功能子集一样,你常用的语言特性或许也只有全部语言特性的 10%。那么,你用不到的这 90% 就都是鸡肋吗?

最后,这个问题可以反映出,前端社区中正在逐渐流行一种「class 就是不好就是鸡肋」的刻板印象。虽然对今天的 JS 生态而言这种观点某种程度上确实成立,但如果据此对「作为工业界通用心智模型的」class 形成跨语言的偏见,甚至排斥接触那些 JS 之外的语言中基于 class 建立的大量工程成果的话,那就显得有些井底之蛙了。

文章分类
前端