JavaScript 继承的本质之辩:从 Crockford 到 Kyle Simpson,我们真的需要 Class 吗?
本文以《JavaScript 语言精粹》第五章为核心线索,融合《你不知道的 JavaScript》中的原型本质观与行为委托理念,带你从伪类、原型、函数化到部件模式,一步步揭开 JavaScript 继承的底层真相,并在两位 JS 宗师的思辨中重新审视 ES6 class 的诞生与争议。
引言:两位 JavaScript 布道者,共同指向一个原点
如果你在 2014 年的某个深夜打开 Kyle Simpson 的《You Don't Know JS: this & Object Prototypes》,你会读到这样一段近乎愤怒的宣言:
Classes are an optional design pattern for code (not a necessary given), and that furthermore they are often quite awkward to implement in a [[Prototype]] language like JavaScript. ------ Kyle Simpson, Appendix A of this & Object Prototypes
而如果你将时间再往前拨六年,翻开 Douglas Crockford 的《JavaScript 语言精粹》第五章,你会看到他在开头引用莎士比亚《理查二世》中的一句话作为继承章节的题眼------"将一体之物分割为诸多对象"------然后用全章大半篇幅完成一件事:他演示了 JavaScript 中所有的继承方式,然后告诉你,最好的那个根本不叫继承。
两位作者,一个被誉为 JavaScript 的"大宗师"(Brendan Eich 对 Crockford 的称呼),一个以"你其实不懂 JS"系列颠覆了无数开发者的认知。他们身处不同的年代,各自梳理同一套原型系统,得出的核心结论却惊人地一致:JavaScript 不是 Java,在原型语言里强行模拟"类",是对语言本质的背叛。
但事实是,2015 年 class 关键字被正式写入 ECMAScript,今天绝大多数前端开发者都在用 class 写 React 组件、Vue Options。代码写得飞起,很少有人会去想:Crockford 和 Kyle Simpson 说了那么多,他们真的错了吗?我们还要不要遵循"函数化继承"那套范式?
这篇文章希望能做一个回答。我们将以 Crockford 在《JavaScript 语言精粹》第五章总结的五种模式为核心线索,沿着历史演进的路标,一步步走进 JavaScript 继承的底层设计,并在最后与《你不知道的 JavaScript》的原型本质观和行为委托理念进行一次完整的对接。读懂这两本书的交汇点,你可能就真正读懂了这门语言的灵魂。
一、历史起点:JavaScript 从没想过做 Java
1995 年 5 月,Brendan Eich 在公司高层"让这门语言看起来像 Java"的指令下,用十天时间创造了 JavaScript。他的初衷是构建一门"为普通网页设计师设计的脚本语言",用动态类型和原型链来完成浏览器端的简单交互------后来我们都知道了,这门语言走向了完全相反的方向。
但"像 Java"这个指令留下了深刻的历史烙印:new 操作符被写入语法;constructor 的概念被引入;每个函数被创建时,JS 引擎会自动执行类似这样的代码:
javascript
this.prototype = { constructor: this };
Crockford 将这种设计结果称为 "伪类"(Pseudoclassical) ------它用 prototype 和 new 刻意模仿了传统类语言的继承语法,但骨子里仍然是原型委托。
更值得追问的是:Brendan Eich 当初为什么选择原型?
答案出奇地简单。Eich 在设计之初面临两个硬性约束:时间极短(十天)、必须嵌入浏览器环境。原型机制恰好满足这两个需求------只需一个属性链就能实现行为共享,无需复杂的类元编程;而且原型天然适合 DOM 这种树形结构的扩展。可以说,原型不是设计出来的,是被时间逼出来的。
Crockford 在书中对伪类模式提出了严厉且准确的批评:
- 没有私有环境:所有属性都是公开的,无法实现信息隐藏。
- 无法安全访问父类方法 :
super机制缺失(这是 2008 年的 ES3 时代),在多层级链中会造成严重不便。 - 忘记
new的灾难性后果 :如果调用构造器时遗漏new前缀,this不会被绑定到新对象,而是指向全局对象,从而污染全局命名空间------且没有编译警告,也没有运行时错误。
他的最终建议在当年可以说是石破天惊:"最好的方法就是根本不用 new。"
二、原型链的本质:不是"复制",是"委托"
在进入继承模式之前,我们有必要先借助《你不知道的 JavaScript》的视角,重新审视一个被频繁误解的核心概念------原型链。
Kyle Simpson 在 this & Object Prototypes 中反复强调一个关键点:在传统类语言中,继承意味着"复制"------父类的行为被拷贝到子类,再拷贝到实例。而在 JavaScript 的 [[Prototype]] 机制中,继承根本不是复制,而是反向的"委托链接"。
Classes in traditional class-oriented languages actually produce a copy action from parent to child to instance, whereas in [[Prototype]], the action is not a copy, but rather the opposite --- a delegation link. ------ Kyle Simpson, Appendix A of this & Object Prototypes
这意味着什么?
当你访问 obj.foo,JavaScript 首先在 obj 自身查找属性 foo。如果找不到,引擎不会报错,而是顺着 [[Prototype]] 链向上查找,直到找到该属性或到达链的终点(null)。这个过程对开发者透明,但机制本身决定了 JavaScript 的"继承"从根本上就不是类复制。
Crockford 显然早已洞察到这一点。他在书中写道:"JavaScript is a loosely typed language, never casts. The lineage of an object is irrelevant. What matters about an object is what it can do, not what it is descended from."------对象的血统无关紧要,重要的是这个对象能做什么,而非它继承自哪里。 这与 Kyle Simpson 后来提出的"行为委托"(Behavior Delegation)几乎是一体两面。
三、五条道路与一个终点:Crockford 的继承模式全景
《JavaScript 语言精粹》第五章按顺序讨论了五种继承模式。这个顺序并非随意,而是体现了一条从"向外模仿"到"回归本质"的认知递进线索。
3.1 伪类(Pseudoclassical):不应作为首选的历史妥协
这是 Java 程序员最熟悉的模式------用函数作构造器,用 prototype 分发方法。Crockford 提供了一个语法糖 inherits 来隐藏原型操作,但他随后明确指出这种模式"看起来像类但不是类",存在私有性缺失、super 调用不便、忘记 new 破坏全局等致命缺陷。
3.2 对象说明符(Object Specifiers):改变传参方式的编程约定
用单个对象字面量包裹所有构造器参数:
javascript
// ❌ 参数顺序需要死记
var myObject = maker(f, l, m, c, s);
// ✅ 语义自明
var myObject = maker({
first: f,
last: l,
middle: m,
city: c,
state: s
});
这本质上就是当今 JavaScript API 设计中无处不在的 options 对象模式,是每个前端开发者每天都在写的东西。它解决的不是继承本身的问题,而是"如何优雅地传递配置",但它与后文的函数化模式天然适配。
3.3 原型(Prototypal):回归 JavaScript 本质
Crockford 用一章篇幅铺垫伪类的缺陷,然后在原型模式中给出了回应:让对象直接继承对象。
他提供了一个 Object.create 的 polyfill(该 API 后来被 ES5 标准纳入):
javascript
if (typeof Object.create !== 'function') {
Object.create = function(o) {
var F = function() {};
F.prototype = o;
return new F();
};
}
使用方式极为直观------先定义一个基础对象,然后通过 Object.create 创建新对象,只描述与基础对象的差异(Crockford 称之为"差异化继承"):
javascript
var myMammal = {
name: 'Herb the Mammal',
get_name: function() { return this.name; },
says: function() { return this.saying || ''; }
};
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.get_name = function() {
return this.says() + ' ' + this.name + ' ' + this.says();
};
与伪类模式"先定义构造器再替换原型"的做法截然不同,原型模式是"先继承再修改"。这种先有一个实用基础对象、再在它上面定制差异的思维方式,更直观地体现了原型链的工作方式。
3.4 函数化(Functional):用闭包封装真正的私有性
这是全章最核心、最具思想深度的模式,也是 Crockford 最为推崇的完整继承方案。它将前面章节所学的闭包、模块等核心概念综合运用,解决了一个根本问题:实现真正的私有性与封装。
函数化构造器遵循四大步骤:
- 创建一个新对象 ------ 可以用对象字面量、
Object.create或调用其他函数化构造器 - 有选择地定义私有变量和方法 ------ 这是函数内部
var声明的变量,只能被特权函数访问 - 给新对象扩充方法 ------ 这些方法通过闭包获得访问私有变量的特权
- 返回那个新对象
javascript
var mammal = function(spec) {
var that = {};
that.get_name = function() {
return spec.name;
};
that.says = function() {
return spec.saying || '';
};
return that;
};
var myMammal = mammal({ name: 'Herb' });
子类构造器调用父类构造器,在其返回的 that 对象上进行差异化扩展:
javascript
var cat = function(spec) {
spec.saying = spec.saying || 'meow';
var that = mammal(spec);
that.get_name = function() {
return that.says() + ' ' + spec.name + ' ' + that.says();
};
return that;
};
函数化模式的代价是每个实例都会创建独立的方法副本,内存占用略高;但它带来的好处是本质性的:彻底消除了 new 和 this 的复杂绑定问题,同时提供了真正的私有环境。
3.5 部件(Parts):跳出继承框架的终极解法
Crockford 在最后一节用一个能够为任何对象添加事件处理能力的 eventuality 函数给出了高阶思考:不必通过层层继承来构建对象的全部能力,而是定义一些独立的功能模块,将它们"混入"到任何需要的对象上。
javascript
var eventuality = function(that) {
var registry = {};
that.fire = function(event) { /* ... */ };
that.on = function(type, method, parameters) { /* ... */ };
return that;
};
调用 eventuality(myObject),任何对象都能获得完整的事件处理能力。这本质上就是 Mixin 或行为组合------功能模块的混入往往比继承链更灵活。"组合优于继承"这一思想在 2008 年就被 Crockford 写进了 JavaScript 的最佳实践。
五种模式的递进逻辑
| 模式 | 核心问题 | Crockford 的态度 |
|---|---|---|
| 伪类 | 如何让 JavaScript 像 Java? | 批判性展示------指出根本缺陷 |
| 对象说明符 | 如何优雅地传参? | 实用的编程约定 |
| 原型 | 如何让对象继承对象? | 回归本质的基础方案 |
| 函数化 | 如何实现私有性? | 最强推荐的完整方案 |
| 部件 | 如何组合而非继承? | 思想升华------超越继承本身 |
从伪类→原型→函数化→部件,每一步都在解决上一步的遗留问题,终点不再是"如何继承",而是"是否需要继承"。
四、对话《你不知道的 JavaScript》:行为委托与 OLOO
如果说 Crockford 的贡献在于他系统梳理了 JavaScript 可用的继承方案并给出了最优推荐,那么 Kyle Simpson 的贡献则在于:他从语言设计的哲学层面,论证了为什么"行为委托"比"类继承"更符合 JavaScript 的天性。
4.1 OLOO:没有类,只有对象的关联
Kyle Simpson 提出了 OLOO(Objects Linked to Other Objects,对象关联)的编程风格。其核心主张是:类是可以选择的设计模式,而非代码组织的唯一正确答案;JavaScript 的 [[Prototype]] 机制的本质就是行为委托。
他在 this & Object Prototypes 第六章给出了一个经典的对照案例:一个 LoginController 和 AuthController 的场景。传统类设计需要三个实体(父类 Controller + 子类 LoginController + 子类 AuthController),还要借助"合成"来处理 AuthController 对 LoginController 的依赖------逻辑复杂、层级冗余。
而用 OLOO 风格重构后,代码结构发生了根本性简化:
javascript
var LoginController = {
errors: [],
getUser: function() { /* ... */ },
validateEntry: function(user, pw) { /* ... */ },
failure: function(err) { /* ... */ }
};
var AuthController = Object.create(LoginController);
AuthController.checkAuth = function() { /* 直接委托到 LoginController */ };
AuthController.checkAuth();
Kyle Simpson 指出这种设计至少有三个优势:首先,不再需要 Basic Controller 父类来"分享"行为,委托本身就是足够强大的共享机制;其次,不需要 new 实例化------因为这里根本没有类,只有对象自身;最后,AuthController 和 LoginController 是水平对等的,它们之间是委托关系而非父子层级关系。
这与 Crockford 的"原型模式 + 部件模式"形成了惊人的呼应------两位作者从不同的路径出发,抵达了同一个结论:对象之间的直接关联,比层层继承更简单、更强大。
4.2 class 语法糖:Croft-Simpson 批判的当代印证
一个无法回避的历史节点:ES6 在 2015 年引入了 class 关键字。
Kyle Simpson 在书末的 Appendix A 中对此做了透彻的解剖。他首先承认 class 确实解决了部分语法丑陋问题------不再需要手动敲 .prototype。但他随即指出,class 并没有改变底层 [[Prototype]] 的委托本质,它只是语法糖。
更深的隐患在于:class 的 extends、super、static 等语法精心营造了"传统类"的错觉,让开发者心安理得地用 Java 的方式写 JS,却完全忽略了原型系统中"复制 vs 委托"的根本差异。对 Crockford 当年列出的伪类缺陷------私有性缺失(ES6 引入 # 私有字段后有所缓解)、super 语义复杂------class 并未给出本质解决方案,它只是让"写伪类"这件事变得更顺手了。
而当我们把 Crockford 的"函数化模式"与 Kyle Simpson 的"OLOO"并置,会发现它们共享同一个核心理念:用闭包管理私有状态,用对象之间的直接关联替代层级化的类体系。 这条思路在 React Hooks(函数式组件用闭包管理状态,用 Hook 组合替代类组件继承)和 Vue 3 Composition API 中得到了当代的延续。
五、现代回声:React Hooks、模块化与组合的胜利
5.1 从函数化模式到 React 函数组件
Crockford 在 2008 年提出的"函数化模式",核心思路是:用闭包封装状态,返回一个携带着方法的普通对象。这不正是 React 函数组件的核心思想吗?
javascript
// Crockford 的函数化模式(2008)
var cat = function(spec) {
var that = {};
that.get_name = function() { return spec.name; };
return that;
};
// React Hooks 的函数组件(2019)
function Cat({ name }) {
const [mood, setMood] = useState('happy');
return { purr: () => `${name} purrs ${mood}ly` };
}
二者的共同点显而易见:私有状态通过闭包保护,外部只能通过暴露的方法/返回值访问;不依赖 this,不存在 this 绑定丢失的问题;不需要 new,函数调用即可创建实例。
React 当年从 Class Component 转向 Hooks 的决策,本质上是一次"从伪类到函数化"的范式迁移------只是这次,整个前端社区终于买账了。而事实上,Crockford 早在十多年前就把这条路指了出来。
5.2 部件模式 = 今日的 Mixin / 组合模式
Crockford 的 eventuality 函数与 Vue 2 的 Mixin 系统、React 的高阶组件(HOC)本质上是同一个模式:将独立的功能模块注入到任意对象或组件上。Vue 3 在引入 Composition API 后虽然弱化了 Mixin 的使用,但 composables 的核心理念------"提取可复用的功能逻辑,按需组合"------与 Crockford 的部件模式如出一辙。
5.3 当"继承"不再成为唯一答案
回顾整个历程,一个清晰的趋势浮出水面:前端社区正在从"继承为王"向"组合为王"转型。
- 函数化模式 → React Hooks
- 部件模式 → Composables / Hooks / Mixins
- 原型继承 → Object.create + 行为委托
而 class 继承虽然在 Angular、NestJS 等框架中依然占有重要地位,但整体趋势已经不可逆转:"类"不再是组织代码的唯一方式,"组合"正在成为新的主流范式。
结语:理解继承的真正目的
回到本文开篇的问题:Crockford 和 Kyle Simpson 说了那么多,我们还要不要遵循"函数化继承"范式?
我的回答是:不一定要照搬代码,但一定要理解他们的思考方式。
2008 年的函数化模式在今天看来确实有些"过时"------每个实例创建独立方法副本带来的内存开销,在动辄数千实例的场景下不容忽视。原型上的方法共享仍然有其性能优势。但 Crockford 和 Kyle Simpson 留给我们的真正遗产,不是某一段具体的代码,而是一种设计哲学:
继承的终极目的不是建立类型层级,而是实现代码重用。 如果你能用组合实现同样的效果,就不要用继承制造复杂性。
JavaScript 从来不是 Java。它是一门原型语言,这门语言的灵魂不在 class 里,而在 [[Prototype]] 那条看不见的委托链中。当你理解了对象之间可以水平对等地相互委托,你就不必再在垂直的继承层级里挣扎。
或许这就是 Crockford 在章首引用莎翁那句"将一体之物分割为诸多对象"时想表达的含义------真正的智慧,不在于如何分割,而在于如何在分割之后,让对象之间依然保持最优雅的关联。
参考文献
- Douglas Crockford. JavaScript: The Good Parts. O'Reilly Media, 2008.
- Kyle Simpson. You Don't Know JS: this & Object Prototypes. O'Reilly Media, 2014.
- MDN Web Docs. 继承与原型链. Mozilla, 2024.
- Kyle Simpson. 第六章:行为委托 ------ 更简单的设计. You Don't Know JS, 2014.