人人都会AI编程

4.5 常见继承方案的原理与优缺点

更新时间:2026-07-11

JavaScript 的继承机制建立在原型链之上,但在实际开发中,单纯的“原型链继承”并不能解决所有问题(如引用类型共享、无法向父类构造函数传参等)。为此,社区在原型继承的基础上演化出了一系列继承方案,每一种都是对前一方案的修补和改进。理解这些方案的演化过程,不仅能让你知道 ES6 Class 为什么那样设计,更能帮助你在面试、源码阅读和特殊场景下做出正确选择。

下面我们按演进顺序逐一拆解七种继承方案。


4.5.1 原型链继承

原理:将子类的原型对象指向父类的一个实例,通过原型链实现属性和方法的共享。

function Parent() {
  this.name = 'Parent';
  this.colors = ['red', 'blue'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child() {}
Child.prototype = new Parent(); // 核心:将子类原型指向父类实例

const child1 = new Child();
const child2 = new Child();
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue', 'green'] —— 共享了引用类型!

优点

  • 实现简单,父类方法可以被所有子类实例共享。

缺点

  • 引用类型的属性会被所有实例共享,一个实例修改会影响其他实例。
  • 创建子类实例时无法向父类构造函数传递参数(Child 内部没有调用 Parent,参数只能写死在 new Parent() 里)。

4.5.2 构造函数继承(借用构造函数)

原理:在子类构造函数内部使用 callapply 调用父类构造函数,将父类的实例属性“拷贝”到子类实例上。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  Parent.call(this, name); // 核心:在子类环境中执行父类构造函数
  this.age = age;
}

const child1 = new Child('Tom', 18);
const child2 = new Child('Jerry', 20);
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue'] —— 互不影响
console.log(child1.sayName); // undefined —— 继承不到原型上的方法

优点

  • 解决了引用类型共享问题,每个实例都有独立的属性副本。
  • 可以在子类构造函数中向父类传参。

缺点

  • 方法只能定义在构造函数内部,每个实例都会创建一份方法副本,无法复用函数,浪费内存。
  • 父类原型上定义的方法对子类不可见,违背了“继承方法”的初衷。

4.5.3 组合继承(原型链 + 构造函数)

原理:结合前两种方案,用原型链继承方法,用构造函数继承属性。这是 ES6 之前最常用的继承模式。

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 第二次调用 Parent:继承属性
  this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent:继承方法
Child.prototype.constructor = Child; // 修正构造器指向

const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 'Tom'
child1.colors.push('green');
const child2 = new Child('Jerry', 20);
console.log(child2.colors); // ['red', 'blue'] —— 属性不共享

优点

  • 融合了原型链和构造函数的优点:方法共享、属性独立、可传参。

缺点

  • 父类构造函数被调用了两次:一次在创建子类原型时,一次在子类构造函数内部。这导致子类原型上存在一份多余的父类实例属性(namecolors),又被实例自身属性所屏蔽,造成内存浪费。

4.5.4 原型式继承

原理:基于一个已有对象创建新对象,本质上是对 Object.create() 的模拟。ES5 正式将 Object.create() 标准化。

function createObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const person = {
  name: 'Default',
  friends: ['Alice', 'Bob']
};

const p1 = createObject(person);
p1.friends.push('Charlie');
const p2 = createObject(person);
console.log(p2.friends); // ['Alice', 'Bob', 'Charlie'] —— 引用类型仍然共享

优点

  • 不需要定义类型,直接从已有对象克隆出新对象,适合快速创建相似对象。

缺点

  • 和原型链继承一样,引用类型属性会被所有克隆对象共享。
  • 无法传递初始化参数,灵活性受限。

4.5.5 寄生式继承

原理:在原型式继承的基础上,用一个工厂函数封装新对象的创建过程,增强新对象的能力(添加属性或方法)。

function createAnother(original) {
  const clone = Object.create(original); // 原型式继承
  clone.sayHi = function() {             // 添加新方法
    console.log('Hi');
  };
  return clone;
}

const person = {
  name: 'Default',
  friends: ['Alice']
};
const another = createAnother(person);
another.sayHi(); // 'Hi'

优点

  • 可以方便地对已有对象进行扩展,适合仅需扩展单个对象的场景。

缺点

  • 方法和属性依然在对象层面,无法像构造函数那样实现函数复用(每次调用工厂函数都会创建新的 sayHi 方法)。

4.5.6 寄生组合式继承(最优方案)

原理:解决组合继承中“父类构造函数被调用两次”的问题。核心思路:不让子类原型等于父类实例,而是让子类原型指向一个以父类原型为原型的干净对象。然后通过构造函数继承属性。

function inheritPrototype(Child, Parent) {
  const prototype = Object.create(Parent.prototype); // 创建父类原型的副本
  prototype.constructor = Child;                      // 修正 constructor
  Child.prototype = prototype;                        // 将子类原型指向该副本
}

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 只调用一次父类构造函数,继承属性
  this.age = age;
}

inheritPrototype(Child, Parent); // 继承方法

const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'

优点

  • 只调用一次父类构造函数,避免了子类原型上出现多余的属性。
  • 原型链保持正确,方法得到共享,属性保持独立。
  • 是 ES6 Class extends 底层的核心实现模式。

缺点

  • 实现步骤相对多,需要封装一个辅助函数。不过在工程中这通常被一次性封装好。

补充说明:寄生组合式继承是公认的 JavaScript 中最理想的继承范式,也是 Babel 等工具将 ES6 Class 转译为 ES5 时所使用的核心手法。


4.5.7 ES6 Class 继承

原理class / extends / super 是 ES6 提供的语法糖,底层仍基于原型链。但它的实现比纯手写的寄生组合式更严格,加入了 super 调用顺序、不可枚举的方法定义等细节。

class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
  }
  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须先调用 super,才能访问 this
    this.age = age;
  }
}

const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 'Tom'
console.log(child1 instanceof Parent); // true

Class 继承与寄生组合式的对应关系

| 步骤 | 寄生组合式实现 | ES6 Class 实现 |
|------|----------------|----------------|
| 建立原型链 | Child.prototype = Object.create(Parent.prototype) | class Child extends Parent 内部自动完成 |
| 构造函数中调用父类 | Parent.call(this, ...args) | super(...args) |
| 修正 constructor | 手动赋值 | 自动设置 |
| 静态属性/方法继承 | 需额外处理 Child.proto = Parent | 自动继承静态成员 |

优点

  • 语法清晰易读,符合传统面向对象语言的思维习惯。
  • 内置安全性,如 super 前不能使用 this
  • 静态方法和属性会被自动继承。

缺点 / 本质提醒

  • 容易给新开发者造成“JavaScript 是基于类的语言”的错觉。一旦遇到原型链相关的怪异表现,调试起来会非常困惑。因此,理解其底层原型机制仍是必要的。

总结对比与选型建议

| 方案 | 属性共享问题 | 方法复用 | 可传参 | 调用父类次数 | 推荐度 |
|------|-------------|---------|--------|-------------|--------|
| 原型链继承 | ❌ 共享 | ✅ 复用 | ❌ 不可传参 | 1 | 不推荐 |
| 构造函数继承 | ✅ 独立 | ❌ 不复用 | ✅ 可传参 | 1 | 不推荐 |
| 组合继承 | ✅ 独立 | ✅ 复用 | ✅ 可传参 | 2(浪费) | 可用但已过时 |
| 原型式继承 | ❌ 共享 | ✅ 复用 | ❌ 不可传参 | 0 | 特殊场景 |
| 寄生式继承 | ❌ 共享 | ❌ 不复用 | ❌ 不可传参 | 0 | 特殊场景 |
| 寄生组合式继承 | ✅ 独立 | ✅ 复用 | ✅ 可传参 | 1 | 强烈推荐(ES5) |
| ES6 Class 继承 | ✅ 独立 | ✅ 复用 | ✅ 可传参 | 1(super) | 现代开发首选 |

实战建议

  • 新项目直接使用 ES6 Class 继承,代码简洁且不易出错。
  • 理解寄生组合式继承的原理,有助于读懂旧代码、源码和 Babel 编译输出。
  • 原型式继承和寄生式继承在实现部分设计模式(如组合模式扩展)时偶尔会用到,了解即可。

掌握这七种方案后,你将能从容应对任何与原型继承相关的场景,并为后续学习 Vue/React 组件继承、Mixin 混入等前端核心概念打下坚实基础。