JavaScript实现继承
JavaScript 是基于原型链的语言,继承主要通过原型(prototype)来实现。
1. 原型链继承
核心:将父类的实例作为子类的原型。
javascript
function Parent() {
this.name = 'parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
this.age = 10;
}
Child.prototype = new Parent(); // 核心
Child.prototype.constructor = Child; // 修正构造函数指向
const child1 = new Child();
const child2 = new Child();
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue', 'green'] → 引用类型被共享!优点:简单,继承了父类原型上的方法和父类实例属性。
缺点:
- 所有子类实例共享父类实例的引用类型属性(如数组、对象)。
- 创建子类实例时无法向父类构造函数传参。
2. 构造函数继承(伪造对象/经典继承)
核心:在子类构造函数中调用父类构造函数,并绑定 this。
javascript
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', 10);
const child2 = new Child('Jerry', 12);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] → 不共享,每个实例独立
child1.sayName(); // 报错!没有继承父类原型上的方法优点:
- 解决了引用类型共享问题。
- 可以向父类传递参数。
缺点:
- 方法定义在父类构造函数中(如果写在
Parent内部,每个实例会重复创建方法,浪费内存)。 - 无法继承父类原型上的方法,子类实例只是“拷贝”了父类实例的属性。
3. 组合继承(原型链 + 构造函数)
核心:用构造函数继承属性,用原型链继承方法。这是最常用的经典继承模式。
javascript
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); // 第一次调用 Parent,得到实例属性
this.age = age;
}
Child.prototype = new Parent(); // 第二次调用 Parent,得到原型方法
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Tom', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // 'Tom'优点:融合了原型链和构造函数的优点,实例属性独立,方法共享。
缺点:调用了两次父类构造函数,造成一定的性能浪费(子类原型上多了一份多余的父类实例属性,会被实例属性覆盖)。
4. 原型式继承(Object.create)
核心:基于已有的对象创建新对象,本质上是对传入对象的一次浅复制。
javascript
const parent = {
name: 'parent',
colors: ['red', 'blue'],
sayName() { console.log(this.name); }
};
const child1 = Object.create(parent);
const child2 = Object.create(parent);
child1.name = 'Tom';
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue', 'green'] → 引用类型依然共享优点:简单,无需定义构造函数,适合对象直接继承。
缺点:和原型链继承一样,引用类型属性会被所有实例共享。
Object.create的第二个参数可以定义额外属性(与Object.defineProperties相同)。
5. 寄生式继承
核心:在原型式继承的基础上,增强对象(添加方法)。
javascript
function createAnother(original) {
const clone = Object.create(original);
clone.sayHi = function() { // 增强对象
console.log('hi');
};
return clone;
}
const parent = { name: 'parent', colors: ['red'] };
const child = createAnother(parent);
child.sayHi(); // 'hi'优点:适合主要关注对象,而不是类型和构造函数的场景。
缺点:方法在每个实例上都会创建一遍,不能复用;引用类型依然共享。
6. 寄生组合式继承(最理想的继承模式)
核心:通过寄生方式,砍掉父类实例属性,只继承原型。只调用一次父类构造函数。
javascript
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建父类原型的副本
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;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Tom', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // 'Tom'优点:
- 只调用一次父类构造函数,效率高。
- 原型链干净(子类原型上没有多余的父类实例属性)。
- 实例属性独立,方法共享。
- 被认为是最理想的继承方式。
7. ES6 Class 的 extends 继承
ES6 引入了 class 和 extends 关键字,底层依然是寄生组合式继承,但语法更清晰。
javascript
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); // 相当于 Parent.call(this, name)
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const child = new Child('Tom', 10);
child.colors.push('green');
console.log(child.colors); // ['red', 'blue', 'green']
child.sayName(); // 'Tom'优点:语法简洁、易读,内部实现最优(寄生组合式)。
注意:
- 子类必须在
constructor中调用super(),否则this不可用。 - 可以重写父类方法,并通过
super.method()调用父类方法。
8. 继承方式对比总结
8.1 对比表格
| 方式 | 实例属性独立 | 方法共享 | 可传参 | 调用父类次数 | 推荐程度 |
|---|---|---|---|---|---|
| 原型链继承 | ❌ | ✅ | ❌ | 1 | ❌ |
| 构造函数继承 | ✅ | ❌(方法无法共享) | ✅ | 1(每次实例化) | ❌ |
| 组合继承 | ✅ | ✅ | ✅ | 2 | ⚠️(有缺陷但可用) |
| 原型式继承 | ❌ | ✅(通过原型) | ❌ | 0 | ⚠️(适合简单对象) |
| 寄生式继承 | ❌ | ❌(方法不共享) | ❌ | 0 | ⚠️ |
| 寄生组合式 | ✅ | ✅ | ✅ | 1 | ✅(最佳) |
| ES6 extends | ✅ | ✅ | ✅ | 1 | ✅✅(最推荐) |
8.2 面试回答建议
- JavaScript 实现继承有七种常见方式,从最初的原型链继承到寄生组合式继承,再到 ES6 的
class extends。最核心的问题是引用类型共享和方法复用。寄生组合式继承被认为是最理想的传统实现,而现代开发中直接使用 ES6 class 即可,它底层就是寄生组合式继承,语法更友好。 - 如果面试官追问底层,可以补充:
extends内部通过Object.setPrototypeOf或Object.create建立原型链,子类的super相当于Parent.call(this)。