JavaScript 继承方式对比
原型链、构造函数、组合继承、寄生组合、ES6 Class 的实现与优缺点
问题
JavaScript 有哪几种继承方式?各自的优缺点是什么?
解答
1. 原型链继承
function Parent() {
this.colors = ['red', 'blue'];
}
Parent.prototype.getColors = function() {
return this.colors;
};
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'] - 引用类型被共享
优点:简单,能继承父类原型上的方法
缺点:
- 引用类型属性被所有实例共享
- 无法向父类构造函数传参
2. 构造函数继承
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;
}
// 测试
const child1 = new Child('Tom', 18);
const child2 = new Child('Jerry', 20);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] - 不会被共享
console.log(child1.getName); // undefined - 无法继承原型方法
优点:
- 避免引用类型属性共享
- 可以向父类传参
缺点:
- 无法继承父类原型上的方法
- 方法在构造函数中定义,每次创建实例都会创建一遍
3. 组合继承
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 child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child.colors); // ['red', 'blue']
优点:融合原型链和构造函数的优点
缺点:父类构造函数被调用两次,子类原型上有多余的父类属性
4. 寄生组合继承(推荐)
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;
}
// 核心:创建父类原型的副本,避免调用父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 测试
const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child instanceof Parent); // true
优点:
- 只调用一次父类构造函数
- 原型链保持完整
- 是最理想的继承方式
5. ES6 Class 继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
getName() {
return this.name;
}
static sayHello() {
console.log('Hello');
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须先调用 super
this.age = age;
}
getAge() {
return this.age;
}
}
// 测试
const child = new Child('Tom', 18);
console.log(child.getName()); // 'Tom'
console.log(child.getAge()); // 18
Child.sayHello(); // 'Hello' - 静态方法也能继承
优点:
- 语法清晰,易于理解
- 内置支持静态方法继承
- 本质是寄生组合继承的语法糖
缺点:
- 需要 ES6 环境或转译
对比总结
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链 | 简单 | 引用类型共享,无法传参 |
| 构造函数 | 可传参,不共享 | 无法继承原型方法 |
| 组合继承 | 功能完整 | 调用两次父类构造函数 |
| 寄生组合 | 效率高,功能完整 | 实现稍复杂 |
| ES6 Class | 语法简洁 | 需要 ES6 环境 |
关键点
- 原型链继承:
Child.prototype = new Parent(),问题是引用类型共享 - 构造函数继承:
Parent.call(this),问题是无法继承原型方法 - 组合继承:结合两者,但父类构造函数执行两次
- 寄生组合继承:用
Object.create(Parent.prototype)替代new Parent(),是最优方案 - ES6 Class:
extends+super(),本质是寄生组合继承的语法糖
目录