在 JavaScript 中,继承是实现代码复用和类型扩展的核心机制。组合继承(Combination Inheritance) 和寄生继承(Parasitic Inheritance)是两种经典模式
组合继承(Combination inheritance)
组合继承是结合了构造函数继承和原型链继承的方法,是早期最常用的继承方式。
构造函数继承解决了实例属性的问题,原型链继承解决了方法共享的问题。
但是组合继承有一个缺点,就是会调用两次构造函数:一次是在创建子类继承时;一次是在子类构造函数中,这将导致子类原型上有一份多余的父类实例属性。
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;
}
// 原型链继承:子类原型指向父类实例(共享方法)
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;
Child.prototype.sayAge = function () {
console.log(this.age);
};
优点
- 实例属性独立(通过构造函数复制),避免了引用类型共享的问题
- 方法共享(通过原型链),节省空间
缺点
- 父类构造函数被调用两次:第一次是在
new Parent()(创建子类原型时),第二次是在Parent.call(this)(子类构造函数中)
这样会导致子类原型上存在冗余的父类实例属性,浪费内存且可能引发意外覆盖。
寄生继承(Parasitic inheritance)
寄生继承基于原型链继承,但通过一个新对象来增强原型,避免了组合继承两次调用父类构造函数的问题。寄生继承通常通过使用 Object.create() 来创建父类原型的副本,然后添加额外的属性和方法,最后返回这个新对象作为子类的原型。
function createAnother(original) {
// 创建原对象的副本(原型链继承)
const clone = Object.create(original);
// 增强副本(添加新方法)
clone.sayHi = function () {
console.log('Hi');
};
// 返回增强后的对象
return clone;
}
const parent = {
name: 'Parent',
colors: ['red', 'blue'],
};
// child 是增强后的 parent 副本
const child = createAnother(parent);
// "Hi"
child.sayHi();
// 不影响原 parent 对象(引用类型独立?不,原型链共享!)
child.colors.push('green');
优点
- 避免了直接调用父类构造函数,减少冗余属性
- 灵活增加对象(添加/修改方法)
缺点
- 本质上是原型链继承,引用类型属性仍共享
- 更是个“对象增强”场景,而非类式继承
寄生组合式继承
组合继承的优化版本,通过寄生方式继承原型,避免父类构造函数被调用多次。
优先使用 Object.create() 创建纯净的[原型链]:
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 手动修改 constructor 的指向
实例
/// 组合 + 寄生继承
function inheritPrototype(Child, Parent) {
// 创建父类原型的副本(避免实例化父类)
const prototype = Object.create(Parent.prototype);
// 修复 constructor 指向
prototype.constructor = Child;
// 子类原型指向副本
Child.prototype = prototype;
}
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;
}
// 寄生继承原型(避免两次调用 Parent)
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
console.log(this.age);
};
优点
- 父类构造函数仅调用一次:子类实例属性独立,原型链通过副本继承方法
- 无冗余的属性,内存更高效
- 兼容原型继承链继承和方法共享的特性
其他继承
除了组合继承和寄生继承外,还有原型链继承、构造函数继承、拷贝继承、ES6 class 继承等多种模式。
原型链继承(prototype chain inheritance)
- 核心逻辑:让子类的原型对象(prototype)指向父类的实例,通过原型链访问父类的属性和方法
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child() {}
// 子类原型指向父类实例
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;
const child1 = new Child();
child1.sayName(); // "Parent"
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
const child2 = new Child();
console.log(child2.colors); // ["red", "blue", "green"](引用类型共享)
优点
- 直接简单,通过原型链自动继承原型上的方法
- 子类实例可直接访问父类原型的属性
缺点
- 引用类型属性共享:父类实例的引用类型属性会被所有的实例共享,修改一个实例的属性将影响其他的实例
- 无法向父类构造函数传参:父类实例化时无法接收子类传递的参数(因为
new Parent()是固定的) - 父类构造函数副作用:如父类构造函数内有副作用(如修改全局状态),会被意外触发
构造函数继承(Constructor inheritance)
构造函数继承是经典继承。
- 核心逻辑:在子类构造函数中调用父类构造函数(使用
call/apply),将父类属性绑定到子类实例上。
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('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
const child2 = new Child('Bob', 12);
console.log(child2.colors); // ["red", "blue"](引用类型独立)
child2.sayName(); // 报错:sayName 未定义(未继承原型方法)
优点
- 实例属性独立:父类实例属性通过
Parent.call(this)绑定到子类实例,避免了引用类型共享问题 - 支持向父类传参:可通过
Parent.call(this, arg)向父类构造函数传递参数 - 无父类构造函数副作用:父类构造函数仅在子类实例化时调用,无额外的副作用
缺点
- 无法继承父类原型方法:父类上的原型方法无法被子类实例访问
- 方法无法复用:若父类包含多个实例方法,需在子类构造函数中重复定义,无法复用
拷贝继承(Copy inheritance)
- 核心逻辑:通过遍历父类实例和原型的属性,手动复制到子类实例或原型上
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child() {
const parent = new Parent();
// 复制父类实例属性到子类实例(浅拷贝)
Object.assign(this, parent);
}
// 复制父类原型方法到子类原型(浅拷贝)
Child.prototype = Object.assign({}, Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child();
child.sayName(); // "Parent"
child.colors.push('green');
console.log(child.colors); // ["red", "blue", "green"](引用类型仍共享)
优点
- 属性完全独立:如使用深层拷贝,可完全避免引用类型共享问题
- 灵活性高:可选择性复制需要的属性和方法
缺点
- 性能差:深拷贝大对象时开销极大
- 动态方法丢失:父类原型上动态新增的方法无法被复制
- 实现复杂:需手动处理原型链和实例属性的拷贝,易出错
ES6 class 继承
- 核心逻辑:ES6 引入
class关键字,通过extends实现继承,底层基于寄生组合式继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
// 调用父类构造函数(必须先调用)
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
const child2 = new Child('Bob', 12);
console.log(child2.colors); // ["red", "blue"](引用类型独立)
child2.sayName(); // "Bob"(继承父类原型方法)
优点
- 语法简介:无需手动处理原型链、constructor 指向或 super 调用
- 自动优化继承:底层使用继承组合式继承,无冗余属性,性能高效
- 支持静态方法继承:父类静态方法(如
static fn())会被子类继承 - 更符合直觉:类式语法更贴近于传统的面相对象语言
缺点
- 依赖引擎支持:需要在支持 ES6 的环境中使用(现代浏览器或 Node.js ≥ 6)
- 本质仍是原型继承:所有的方法仍定义在原型上,与 ES5 的继承无本质上的区别
需关注的点
-
- 谨慎使用[原型链]继承!直接继承另一个对象的原型将导致:
- 原型链上的引用属性将会被所有的实例共享,可能导致一个实例修改了这个属性,影响到其他的实例;
- 创建子类实例时,无法向父类构造函数传递参数
-
- 组合继承(伪经典继承):几个了构造函数和原型链继承,通常是被认为比较理想的继承方法。但,组合继承会调用两次父类构件函数(一次在创建子类原型时,一次在子类构造函数内部),导致子类的原型上多一分多余的父类实例属性。
-
- 原型继承:基于已有的对象创建新对象,而不必创建自定义类型。
Object.create()就是基于原型式继承。适用于不需要单独创建构造函数的场景;
- 原型继承:基于已有的对象创建新对象,而不必创建自定义类型。
-
- 寄生式继承:创建一个仅用于封装继承过程的函数,在函数内部增强对象,最后返回对象。适合主要关注对象,而不在乎类型和结构函数的场景;
-
- 寄生组合继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不过为了制定子类型的原型而调用父类的构造函数,而是使用父类原型的一个副本。这是最理想的继承方式之一。
-
- 虽然 ES6 引入了
class关键字,但它只是原型链继承的语法糖。class 的 extends 关键字实际上创建了一个原型链继承关系;
- 虽然 ES6 引入了
-
- 在 ES 中,子类构造函数必须先调用
super()才能使用this。super指向父类的原型对象(在方法中)或父类构造函数(在构造函数中);
- 在 ES 中,子类构造函数必须先调用
继承对比
| 继承方式 | 核心机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 原型链继承 | 子类原型指向父类实例 | 简单,自动继承原型方法 | 应用类型共享,无法传参 | 简单原型方法继承 |
| 构造函数继承 | 子类构造函数调用父类构造函数 | 实例属性独立,支持传参 | 无法继承原型方法 | 需独立实例属性的场景 |
| 寄生继承 | 创建父类副本并增强 | 灵活增强对象 | 引用类型共享,适合对象增强 | 对象增强(非类继承) |
| 组合继承 | 构造函数 + 原型链 | 实例独立 + 方法共享 | 父类构造函数调用两次,原型冗余 | 理解底层机制的学习场景 |
| 寄生组合式继承 | 寄生 + 组合 | 无冗余、高效 | 实现稍复杂 | 手动实现类继承的最优解 |
ES 6 class继承 | 语法糖(底层寄生组合式) | 简介高效,支持静态方法 | 依赖于 ES6 环境 | 现代项目首选 |
| 拷贝继承 | 手动复制属性/方法 | 属性独立(深拷贝时) | 性能差,动态方法丢失(非继承式继承) | 特殊需求(如需隔离环境) |