JavaScript继承的8种方式,你知道几种?

502 阅读6分钟

JS继承别再只会 extends 了,原型链继承、借用构造函数继承、组合继承、寄生式继承、寄生组合式继承......这些你都了解吗?

1. 原型链继承

构造函数、原型和实例之间的关系

  • 每个构造函数都有一个原型对象(protype)
  • 原型对象都包含一个指向构造函数的指针(constructor)
  • 实例都包含一个指向构造函数原型对象的指针,也叫隐式对象(__proto__)。

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

1.1. 实现

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType(); 

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

const instance = new SubType();
console.log(instance.getSuperValue()); // true
console.log(instance.getSubValue()); // false
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
console.log(Object.getPrototypeOf(SubType.prototype) === SuperType.prototype); // true

1.2. 优缺点分析

  • 优点:父类的方法可以复用
  • 缺点多个实例对 引用类型 的操作会被篡改 (原始类型不受影响);子类示例不能给父类构造函数传参
function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {}

SubType.prototype = new SuperType()

const instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) //"red,blue,green,black"

const instance2 = new SubType()
console.log(instance2.colors) //"red,blue,green,black"

2. 借用构造函数继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

2.1. 实现

function SuperType() {
  this.color = ['red', 'green', 'blue']
}
function SubType() {
  //继承自SuperType
  // 核心代码是SuperType.call(this)
  // 创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份。
  SuperType.call(this)
}
const instance1 = new SubType()
instance1.color.push('black')
console.log(instance1.color) //"red,green,blue,black"

const instance2 = new SubType()
console.log(instance2.color) //"red,green,blue"

2.2. 优缺点分析

  • 优点:父类引用类型的数据不会被子类共享,不会相互影响

  • 缺点

    • 只能继承父类的实例属性和方法,不能继承父类原型属性/方法
    • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

3. 组合继承

组合上述两种方法就是组合继承:

  • 用原型链实现对原型属性和方法的继承
  • 用借用构造函数技术来实现实例属性的继承。

3.1. 实现

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name)
  this.age = age
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType()
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
  console.log(this.age)
}

const instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) //"red,blue,green,black"
instance1.sayName() //"Nicholas";
instance1.sayAge() //29

const instance2 = new SubType('Greg', 27)
console.log(instance2.colors) //"red,blue,green"
instance2.sayName() //"Greg";
instance2.sayAge() //27

3.2. 优缺点分析

  • 优点

    • 父类可以复用:子类实例可以访问父类原型上的属性和方法
    • 父类构造函数中引用类型的数据不会被共享,就不会被随意篡改
  • 缺点会调用两次父类的构造函数,子类实例会有两份一样的属性和方法,影响性能

4. 原型式继承

利用一个空对象作为中介将某个对象直接赋值给空对象构造函数的原型

4.1. 实现

object()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。

Object.create() 的方法,能够代替代码中的object方法

function object(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}
const person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van'],
}

const anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

const yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')

console.log(person.friends) //"Shelby,Court,Van,Rob,Barbie"

4.2. 优缺点分析

  • 缺点

    • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
    • 无法传递参数

5. 寄生式继承

在原型式继承的基础上,增强对象,返回构造函数

5.1. 实现

function object(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}
function createAnother(original) {
  const clone = object(original) // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式来增强对象
    console.log('hi')
  }
  return clone // 返回这个对象
}
const person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van'],
}
const anotherPerson = createAnother(person)
anotherPerson.sayHi() //"hi"

5.2. 优缺点分析

  • 缺点(同原型式继承):

    • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
    • 无法传递参数

6. 寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承。

目前最优的方案,最成熟的方法,也是现在库实现的方法

6.1. 实现

function inheritPrototype(subType, superType) {
  const prototype = Object.create(superType.prototype) // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  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)
}

const instance1 = new SubType('xyc', 23)
const instance2 = new SubType('lxy', 23)

instance1.colors.push('2') // ["red", "blue", "green", "2"]
instance1.colors.push('3') // ["red", "blue", "green", "3"]
console.log(instance1.colors)
console.log(instance2.colors)
instance1.sayName()
instance1.sayAge()
instance2.sayName()
instance2.sayAge()

6.2. 优缺点分析

  • 优点
    • 只需要调用一次父类构造函数
    • 保持原型链不变

7. 混入方式继承多个对象

function MyClass() {
  SuperClass.call(this)
  OtherSuperClass.call(this)
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype)
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype)
// 这一步会把constructor覆盖掉,需要重新指定
// 重新指定constructor
MyClass.prototype.constructor = MyClass

MyClass.prototype.myMethod = function () {
  // do something
}

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

8. ES6类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法。

8.1. 使用

class Rectangle {
  // constructor
  constructor(height, width) {
    this.height = height
    this.width = width
  }

  // Getter
  get area() {
    return this.calcArea()
  }

  // Method
  calcArea() {
    return this.height * this.width
  }
}

const rectangle = new Rectangle(10, 20)
console.log(rectangle.area)
// 输出 200

// 继承
class Square extends Rectangle {
  constructor(length) {
    super(length, length)

    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square'
  }

  get area() {
    return this.height * this.width
  }
}

const square = new Square(10)
console.log(square.area)
// 输出 100

8.2. 实现原理

extends继承的实现和寄生组合式继承方式一样

function _inherits(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })

  if (superType) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subType, superType)
      : (subType.__proto__ = superType)
  }
}

9. 总结

9.1. 函数声明和类声明的区别

函数声明会提升,类声明不会

9.2. ES5继承和ES6继承的区别

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上( Parent.call(this)
  • ES6的继承有所不同,实质上是先创建父类的实例对象this ,然后再用子类的构造函数修改this 因为子类没有自己的this对象,所以必须先调用父类的super() 方法,否则新建实例报错