好的 — 我把 “JS 最理想的继承 —— 寄生组合式继承(Parasitic Combination Inheritance)” 给你做成一份清晰、可直接用的讲解(含 ES5 实现、原理、优缺点与 ES6 对应写法)。开干 🚀
什么是寄生组合式继承(Parasitic Combination Inheritance)
寄生组合式继承是 ES5 中实现类式继承的一种经典并且被广泛认为“比较理想”的模式。它结合了 构造函数盗用(constructor stealing) 和 原型链继承(prototype chaining) 的优点,同时避免了原型链继承中 父构造函数被调用两次、以及共享引用类型属性的问题。
核心思想:
- 在子构造函数中通过
Parent.call(this, ...)
将父类的实例属性复制到子实例(解决引用类型属性共享问题)。 - 通过
Object.create(Parent.prototype)
创建子类原型,避免直接new Parent()
导致父构造执行两次(提高性能并避免副作用)。 - 修正
constructor
指向并可添加子类原型方法。
ES5 标准实现(推荐的模板)
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']; // 引用类型属性
}
Parent.prototype.sayName = function() {
return this.name;
};
// 子类构造函数
function Child(name, age) {
// 1. 盗用构造函数:把父类实例属性复制到子实例
Parent.call(this, name);
this.age = age;
}
// 2. 创建一个以 Parent.prototype 为原型的对象,避免 new Parent()
function inheritPrototype(child, parent) {
// Object.create 可直接,但演示兼容写法:
var proto = Object.create(parent.prototype);
proto.constructor = child; // 修正 constructor 指向
child.prototype = proto;
}
// 绑定继承关系
inheritPrototype(Child, Parent);
// 3. 给子类添加方法
Child.prototype.sayAge = function() {
return this.age;
};
// 测试
var c1 = new Child('Alice', 20);
c1.colors.push('yellow');
console.log(c1.name); // Alice
console.log(c1.colors); // ['red','blue','green','yellow']
console.log(c1.sayName()); // Alice
var c2 = new Child('Bob', 22);
console.log(c2.colors); // ['red','blue','green'] -> 不受 c1 影响(符合预期)
上面 inheritPrototype
的关键点是用 Object.create(parent.prototype)
:这样 child.prototype
的原型链是父类的原型,但不会执行 Parent
构造函数(避免了可能的副作用和重复初始化开销)。
为什么叫“寄生”?
“寄生”一词来自于 寄生式继承(parasitic inheritance) 的思想:通过一个辅助函数返回一个包裹对象(或增强对象),在这里 inheritPrototype
起到“寄生”作用:不直接复用父类实例,而是在父类原型之上创建一个“中转”原型对象并增强它。
优点(为什么被称为“较理想”)
- 解决了引用类型属性在多个实例间被共享的问题(通过
Parent.call(this)
)。 - 避免使用
new Parent()
设置Child.prototype
,因此不会执行父构造函数两次(提高性能并避免副作用)。 - 原型方法共享:
Child.prototype
上的方法仍然共享父类原型的方法,不会每个实例复制。 - 兼容 ES5,适用于不使用
class
的旧代码或需要深入理解原型链的场景。
缺点与注意点
- 代码比直接 ES6
class
更啰嗦(可读性稍差)。 - 仍需小心手动修正
constructor
指针。 - 无法使用一些 ES6
class
的特性(如super
关键字、静态私有字段等)。 - 对性能敏感的场景,
Object.create
与函数调用也有微量开销,但通常比new Parent()
好。
和其他继承方式对比(简述)
- 原型链继承(
Child.prototype = new Parent()
):简单但会导致父构造函数执行一次、实例共享引用类型属性 → 不好。 - 构造函数继承(
Parent.call(this)
):解决引用类型共享,但无法继承父原型方法,方法无法复用。 - 组合继承(constructor stealing + 原型链继承):可行,但会导致 父构造函数被调用两次(一次在
Child.prototype = new Parent()
,一次在Parent.call(this)
)。 - 寄生组合继承:综合优点,避免了“被调用两次”的问题 —— 推荐的 ES5 实现方式。
ES6 中的对等写法(class / extends)
在 ES6+ 中,class
/ extends
与 super()
已经把繁琐的细节抽象好,效果等价(并且更规范):
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
sayName() {
return this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 等价于 Parent.call(this, name)
this.age = age;
}
sayAge() {
return this.age;
}
}
ES6 extends
在规范中会在内部以类似寄生组合继承的方式处理原型链与构造函数调用(并有 super
语义),因此在现代代码里优先使用 class
/ extends
,除非你必须兼容 ES5 环境或需要展示原型链细节。
何时用寄生组合继承?
- 你在写 ES5 风格的库/框架,需要兼容旧浏览器(不使用 Babel)。
- 你想教学 / 理解原型继承原理,要展示“为什么继承需要这么做”。
- 你需要手工控制原型链,或在某些低级别场景避免
class
的封装。
快速回顾(一句话摘要)
寄生组合继承 = 在子构造函数中用 Parent.call(this, ...)
继承实例属性 + 用 Object.create(Parent.prototype)
建立原型链(并修正 constructor
),从而达到内存、性能与语义上的平衡。
发表回复