主题
继承
提示
继承是面向对象编程中的核心概念,主要用于提升代码复用性。本文总结了 JavaScript 中常见的继承方式及其优缺点。
在 JavaScript 中,继承主要依赖原型链来实现。
1. 原型链继承
核心思想:将父类的实例作为子类的原型。
js
function SuperType() {
this.property = 'Super'
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
// 关键点:重写子类原型,指向父类实例
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
const instance1 = new SubType()
instance1.colors.push('black')
const instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green', 'black']缺点:
- 引用属性共享:父类实例属性变成子类原型属性,导致所有子类实例共享引用类型数据(如数组),修改一个会影响所有。
- 无法传参:创建子类实例时,无法向父类构造函数传参。
2. 借用构造函数继承 (经典继承)
核心思想:在子类构造函数中调用父类构造函数。
js
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
function SubType(name) {
// 继承 SuperType,并传参
SuperType.call(this, name)
}
const instance1 = new SubType('fengfeng')
instance1.colors.push('black')
console.log(instance1.name) // 'fengfeng'
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']
const instance2 = new SubType('fengfeng')
console.log(instance2.colors) // ['red', 'blue', 'green']优点:
- 解决了引用属性共享问题。
- 可以向父类传参。
缺点:
- 方法无法复用:方法都在构造函数中定义,每次创建实例都会重新创建方法。
- 无法继承原型方法:只能继承父类实例属性/方法,无法访问父类原型上的方法。
3. 组合继承 (伪经典继承)
核心思想:结合原型链继承和借用构造函数继承。使用原型链继承方法,使用构造函数继承属性。
js
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性 (第二次调用父类构造函数)
SuperType.call(this, name)
this.age = age
}
// 继承方法 (第一次调用父类构造函数)
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType // 修复 constructor 指向
SubType.prototype.sayAge = function () {
console.log(this.age)
}优点:
- 融合了前两者的优点,是 ES6 之前最常用的继承模式。
- 保留了
instanceof和isPrototypeOf()的识别能力。
缺点:
- 调用两次父类构造函数:一次在创建子类原型时,一次在子类构造函数内部。导致子类原型上产生多余的父类实例属性。
4. 原型式继承
核心思想:不自定义类型,而是借助一个临时构造函数,将传入的对象作为其原型。ES5 的 Object.create() 规范了这种方式。
js
function object(obj) {
function F() {}
F.prototype = obj
return new F()
}
// 使用
const person = {
name: 'person',
colors: ['red', 'blue', 'green'],
}
const instance1 = object(person) // 或 Object.create(person)
instance1.colors.push('black')
const instance2 = object(person)
console.log(instance2.colors) // ['red', 'blue', 'green', 'black']缺点:
- 与原型链继承类似,引用类型属性会被共享。
5. 寄生式继承
核心思想:创建一个仅用于封装继承过程的函数,在内部以某种方式增强对象,最后返回对象。
js
function createAnother(original) {
const clone = Object.create(original) // 创建新对象
clone.sayHi = function () {
// 增强对象
console.log('hi')
}
return clone // 返回对象
}缺点:
- 与借用构造函数类似,方法无法复用。
6. 寄生组合式继承 (最佳实践)
核心思想:通过借用构造函数继承属性,但使用寄生式继承来继承父类原型(而非调用父类构造函数)。
js
function inheritPrototype(subType, superType) {
// 1. 创建父类原型的副本
const prototype = Object.create(superType.prototype)
// 2. 修正 constructor 指向
prototype.constructor = subType
// 3. 将副本赋值给子类原型
subType.prototype = prototype
}
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // 继承属性
this.age = age
}
inheritPrototype(SubType, SuperType) // 继承方法
SubType.prototype.sayAge = function () {
console.log(this.age)
}优点:
- 只调用一次父类构造函数。
- 避免了在子类原型上创建不必要的属性。
- 原型链保持不变。
- 这是引用类型最理想的继承范式。
7. ES6 类继承 (extends)
ES6 引入了 class 关键字,但其本质依然是基于原型的语法糖。
js
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return `(${this.x}, ${this.y})`
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y) // 调用父类构造函数,必须在 this 之前
this.color = color
}
toString() {
return `${this.color} ${super.toString()}` // 调用父类方法
}
}ES5 vs ES6 继承的区别
- ES5:先创建子类实例对象
this,再将父类的方法添加到this上(Parent.call(this))。 - ES6:先将父类实例对象的属性和方法加到
this上(所以必须先调用super()),然后再用子类构造函数修改this。
Babel 编译原理简析
Babel 将 ES6 的 extends 编译为类似于寄生组合式继承的代码,并处理了 __proto__ 的指向,使得子类构造函数也能继承父类构造函数的静态属性。
js
// 核心逻辑模拟
function _inherits(subClass, superClass) {
// 继承原型方法
subClass.prototype = Object.create(superClass.prototype)
subClass.prototype.constructor = subClass
// 继承静态属性 (ES6 特性)
// subClass.__proto__ = superClass
Object.setPrototypeOf(subClass, superClass)
}