JavaScript 继承方式总结
1. 原型链继承
1.1 实现方式
function Parent() {
this.name = "Parent";
this.friends = [];
}
Parent.prototype.sayHello = function () {
console.log("Hello from " + this.name);
};
function Child() {
this.cardNum = 1001;
}
// 让子类原型指向父类实例
Child.prototype = new Parent();
const child1 = new Child();
const child2 = new Child();
1.2 特点
- 通过
Child.prototype = new Parent() 实现
- 子类实例可以访问父类原型上的方法
1.3 缺点
- 引用类型共享问题:所有子类实例共享同一个父类实例对象的属性,修改一个会影响所有实例
- 无法传参:创建子类实例时无法向父类构造函数传递参数
- 属性污染:父类实例的属性会成为子类原型的属性
2. 构造函数继承
2.1 实现方式
function Parent(name) {
this.name = name;
this.hobbies = ["reading", "music"];
}
function Child(name, age) {
// 在子类构造函数中调用父类构造函数并使用call改变this指向
Parent.call(this, name);
this.age = age;
}
const child1 = new Child("Alice", 10);
const child2 = new Child("Bob", 20);
2.2 特点
- 通过
Parent.call(this, ...) 在子类构造函数中调用父类构造函数
- 每个子类实例都有自己的属性副本
2.3 优点
- 解决引用类型共享问题:每个实例都有独立的属性
- 可以传参:创建子类实例时可以向父类传递参数
2.4 缺点
- 无法继承原型方法:只能继承父类构造函数中的属性和方法,无法访问父类原型上的方法
- 方法重复创建:如果父类构造函数中有方法,每个实例都会创建一份副本
3. 组合继承(组合式继承)
3.1 实现方式
function Parent(name) {
this.name = name;
this.hobbies = ["reading", "music"];
}
function Child(name, age) {
// 在子类构造函数中调用父类构造函数并使用call改变this指向
Parent.call(this, name);
this.age = age;
}
const child1 = new Child("Alice", 10);
const child2 = new Child("Bob", 20);
3.2 特点
- 结合原型链继承和构造函数继承
- 既继承实例属性,又继承原型方法
3.3 优点
- 功能完整:既能继承实例属性,又能继承原型方法
- 解决引用类型共享:通过构造函数继承解决
- 可以传参:支持向父类传递参数
3.4 缺点
- 调用两次父类构造函数:
- 第一次:
Child.prototype = new Parent()
- 第二次:
Parent.call(this, name)
- 原型上有无用属性:第一次调用创建的父类实例的属性都是 undefined,因为真正的属性通过第二次调用绑定到了子类实例上
4. 寄生组合继承(最优方案)
4.1 实现方式
// 原型式继承函数
function createObject(o) {
function Fn() {}
Fn.prototype = o;
return new Fn();
}
// 寄生组合继承核心函数
function inheritPrototype(SubType, SuperType) {
// 创建一个新对象,其原型指向父类原型
SubType.prototype = createObject(SuperType.prototype);
// 修复constructor指向
Object.defineProperty(SubType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: SubType,
});
}
function Parent(name) {
this.name = name;
this.hobbies = ["reading", "music"];
}
Parent.prototype.sayHello = function () {
console.log("Hello from " + this.name);
};
function Child(name, age) {
// 构造函数继承:继承实例属性
Parent.call(this, name);
this.age = age;
}
// 寄生组合继承:继承原型方法
inheritPrototype(Child, Parent);
const child1 = new Child("Alice", 10);
4.2 特点
- 通过创建中间对象避免直接调用父类构造函数来设置原型
- 中间对象的原型指向父类原型,子类原型指向中间对象
4.3 优点
- 只调用一次父类构造函数:避免了组合继承的重复调用问题
- 原型链干净:原型上没有无用的 undefined 属性
- 功能完整:既继承实例属性,又继承原型方法
- 性能最优:是目前最推荐的继承方式
4.4 核心思想
- 用一个"中介对象"来连接子类和父类的原型链
- 避免通过
new Parent() 来设置原型,而是通过原型式继承创建中间对象
继承方式对比总结
| 继承方式 | 继承实例属性 | 继承原型方法 | 引用类型共享 | 可传参 | 调用父类构造函数次数 | 推荐度 |
|---|
| 原型链继承 | ✅ | ✅ | ❌(共享) | ❌ | 1 次 | ⭐ |
| 构造函数继承 | ✅ | ❌ | ✅(独立) | ✅ | 1 次 | ⭐⭐ |
| 组合继承 | ✅ | ✅ | ✅(独立) | ✅ | 2 次 | ⭐⭐⭐ |
| 寄生组合继承 | ✅ | ✅ | ✅(独立) | ✅ | 1 次 | ⭐⭐⭐⭐⭐ |
总结
JavaScript 继承的演进过程体现了逐步解决问题、优化性能的思路:
- 原型链继承 奠定了基础,但有引用类型共享和无法传参的问题
- 构造函数继承 解决了共享和传参问题,但丢失了原型方法继承
- 组合继承 结合两者优点,但存在性能问题(调用两次构造函数)
- 寄生组合继承 是最终的完美解决方案,兼具功能性和性能
在现代开发中,ES6 的 class 语法糖底层实现就是基于寄生组合继承的思想。
检查一下我这四个文件,然后我梳理一下,首先是最基本的原型链继承,通过让Child.prototype = new Parent()实现,缺点就是后续创建的Child对象都会共享new Parent过程中创建的匿名Parent类型对象的属性,如果一个Child对象对属性作修改,会影响所有的Child对象,而且还不能传入参数;然后提出构造函数继承来解决上面的问题,通过调用Child构造函数时使用call绑定this然后执行,让后续通过new Child创建的对象能绑定Parent里的属性,同时还避免了共享属性的问题,还能传参数,但是缺点就是只能有Parent的属性,拿不到Parent原型对象上的方法;然后提出了组合式继承,结合原型链继承和构造函数继承,这样子可以让new Child出来的对象可以绑定到Parent的属性,同时也能使用原型对象上的方法,但是缺点就是创建一个Child至少要调用两次Parent构造函数,然后的话,用到原型链继承的部分创建出来的匿名Parent对象上的属性都是undefined,因为属性都被绑到Child创建的对象上了,然后最终方案就是寄生组合式继承,就是通过创建一个新对象,新对象的原型指向Parent,然后让Child的原型指向这个新对象,同时在new Child的时候还是使用组合式继承里的用call调用Parent的构造函数来绑定一些Parent上的属性,但是这样子的方法可以解决两次调用Parent构造函数,同时也不会因为new Parent导致创建一个匿名Parent对象,里面的属性用不到的情况