别再用 class 装懂继承:寄生组合式才是 JS 的真·继承

71 阅读4分钟

你有没有想过,为什么在 JavaScript 里实现“继承”这件事,能让人又爱又恨?

它没有传统语言中的 class(至少在 ES6 之前没有),却用一套基于原型的机制,默默支撑起了整个前端生态的类式编程。然而,从原型链继承的属性共享陷阱,到构造函数继承的方法冗余,再到组合继承中父类构造函数被无谓调用两次的性能浪费……每一种方案都像是“打补丁”,直到——寄生组合式继承横空出世。

它优雅、高效、语义清晰。如果你曾为继承问题头疼,那么恭喜你,这篇文章将带你揭开 JavaScript 继承的终极答案。

一、继承的痛点:为什么我们需要一个“终极方案”?

在 ES6 的 class 语法糖出现之前,JavaScript 的继承完全依赖函数与原型链的手动拼接。虽然灵活,但每种主流实现方式都带着明显的“伤疤”:

1. 原型链继承:副作用藏不住

function Animal() {}
Animal.prototype.eat = function() { console.log('eating'); };

function Cat() {}
Cat.prototype = new Animal(); // ❌ 隐患:无条件执行了 Animal 构造函数!

问题:父类构造函数被强制执行,可能触发不必要的副作用(比如初始化 DOM、发起网络请求),而且无法向父类传参——这在真实项目中几乎是致命的。

2. 构造函数继承:方法无法复用

function Cat(name) {
  Animal.call(this, name); // ✅ 实例属性正确继承
}

问题:虽然解决了实例属性和参数传递的问题,但完全丢失了对父类原型方法的继承。每个子类实例都要重复定义方法,不仅浪费内存,也违背了原型设计的初衷。

3. 组合继承:看似完美,实则冗余

function Cat(name) {
  Animal.call(this, name); // 第一次调用 Animal
}
Cat.prototype = new Animal(); // 第二次调用 Animal ❌

问题:这是曾经最流行的方案——它结合了前两种的优点,却带来了新的代价:父类构造函数被调用了两次。第一次用于初始化实例属性,第二次只是为了设置原型链。这种冗余在性能敏感或父类构造逻辑复杂的场景中,往往会带来难以忽视的开销甚至潜在 bug。

正是这些“看似可行、实则埋雷”的继承方案,让无数开发者在深夜调试原型链时抓狂——直到寄生组合式继承横空出世,一锤定音,补上了 JavaScript 继承拼图的最后一块!

二、寄生组合式继承:JavaScript 继承的优雅终章

在原型链继承的副作用、构造函数继承的方法冗余、以及组合继承的双重调用等痛点之后,寄生组合式继承(Parasitic Combination Inheritance) 成为公认的最优解:它以最小代价,实现了安全、高效、可维护的继承。

其核心策略清晰而克制:

  • 实例属性通过 Parent.call(this, ...args) 借用初始化
    → 支持传参,每个实例独立,无共享污染。
  • 原型方法通过“空中介函数”桥接继承
    → 不执行父类构造函数,仅复用其原型链。

✅ 实现示例

function Parent(name, age) {
  this.name = name;
  this.age = age;
}
Parent.prototype.greet = function() {
  console.log(`Hi, I'm ${this.name}`);
};

function Child(name, age, grade) {
  Parent.call(this, name, age); // 继承实例属性
  this.grade = grade;
}

// 寄生组合式继承工具函数
function inheritPrototype(Child, Parent) {
  const PrototypeProxy = function() {};
  PrototypeProxy.prototype = Parent.prototype;
  Child.prototype = new PrototypeProxy();
  Child.prototype.constructor = Child;
}

inheritPrototype(Child, Parent);

// 子类扩展
Child.prototype.study = function() {
  console.log(`${this.name} is studying in grade ${this.grade}`);
};

const student = new Child('小明', 10, 4);
student.greet();   // Hi, I'm 小明
student.study();   // 小明 is studying in grade 4
console.log(student instanceof Parent); // true

寄生组合式继承的核心:inheritPrototype

function inheritPrototype(Child, Parent) {
  const PrototypeProxy = function() {};
  PrototypeProxy.prototype = Parent.prototype;
  Child.prototype = new PrototypeProxy();
  Child.prototype.constructor = Child;
}

这四行是整个模式的灵魂,我们逐行拆解:

▪ 第1行:创建一个空的中介构造函数

const PrototypeProxy = function() {};
  • 它不接收参数,也不做任何事,只是一个“壳”。

▪ 第2行:让这个壳的原型指向父类原型

PrototypeProxy.prototype = Parent.prototype;
  • 现在,任何 PrototypeProxy 的实例都能访问 Parent.prototype 上的方法(如 greet)。

▪ 第3行:用这个壳创建子类的原型

Child.prototype = new PrototypeProxy();
  • new PrototypeProxy() 创建了一个新对象,它的 [[Prototype]] 指向 Parent.prototype
  • 但它不是 Parent 的实例,所以不会执行 Parent 构造函数(无副作用、无冗余);
  • 这个新对象成为 Child.prototype,干净、独立、又能访问父类方法。

💡 这等价于:Child.prototype = Object.create(Parent.prototype);

▪ 第4行:修复 constructor 指向

Child.prototype.constructor = Child;
  • 因为 Child.prototype 现在是一个新对象,默认 constructor 指向 PrototypeProxy
  • 修正为 Child,保证 instance.constructor === Child,符合预期,便于调试和反射。

image.png


✅ 结语

寄生组合式继承之所以被誉为“终极方案”,在于它用最克制的设计,解决了 JavaScript 继承的所有历史难题。即便在 class 语法普及的今天,其思想仍深植于现代编译工具(如 Babel)的底层实现中——理解它,就是理解 JavaScript 原型机制的精髓。