JavaScript继承的几种方式

80 阅读4分钟

原型链继承

原型链继承是比较常见的继承方式之一,其中涉及构造函数、原型和实例,三者之间存在一定的关系,每一个构造函数都有一个原型对象,即 prototype 属性的指向,原型对象的 construct 属性又指向构造函数,而实例则通过 __proto__ 属性指向原型对象。

代码如下:

function Parent1 () {
  this.name = 'parent1'
  this.play = [1, 2, 3]
}

function Child1 () {
  this.type = 'child1'
}

Child1.prototype = new Parent1()
const mason = new Child1()
console.log(mason) // Parent1 { type: 'child1' }

这样写虽然可以访问到父类的元素,但是这样写会有问题,比如:

const mason = new Child1()
const jack = new Child1()

mason.play.push(4)
console.log(mason.play) // [ 1, 2, 3, 4 ]
console.log(jack.play) // [ 1, 2, 3, 4 ]

两个实例都指向同一个原型对象,它们的内存空间是共享的,当一个发生了变化,另外一个也会随之发生变化,这就是使用原型链继承方式的一个缺点

构造函数继承(借助call)

function Parent2 () {
  this.name = 'parent2'
}

Parent2.prototype.getName = function () {
  return this.name
}

function Child2 () {
  Parent2.call(this)
  this.type = 'child2'
}

const kim = new Child2()
console.log(kim) // Child2 { name: 'parent2', type: 'child2' }
console.log(kim.getName()) // TypeError: kim.getName is not a function

可以看到上述代码的执行结果 这里除了 Child2 的原本的属性外也继承了 Parent2name 属性,但是只是解决了第一种继承方式的弊端,拥有了自己的内存空间,但是无法继承父类所属的原型链中的属性方法,只能继承父类实例中的属性和方法, hasownpropertytrue 的属性

组合继承

这种方法结合了前两种继承方式,代码如下:

function Parent3 () {
  this.name = 'parent3'
  this.play = [1, 2, 3]
}
Parent3.prototype.getName = function() {
  return this.name
}

function Child3() {
  Parent3.call(this)
  this.type = 'child3'
}

Child3.prototype = new Parent3()

Child3.prototype.constructor = Child3

const a = new Child3()
const b = new Child3()
a.play.push(4)

console.log(a) // Child3 { name: 'parent3', play: [ 1, 2, 3, 4 ], type: 'child3' }
console.log(b) // Child3 { name: 'parent3', play: [ 1, 2, 3 ], type: 'child3' }
console.log(a.getName()) // parent3

执行上面代码,之前两种方法的问题都得到了解决

在构造函数 Child3 中调用 Parent3 并将this指向 Child3 ,即为将 Parent3 中的属性添加到Child3中两个构造函数都拥有了这些属性,后面创建实例,调用属性,会优先使用子类中的属性,并且不会与其他子类共享内存空间

Child3 加入到 Parent3 中的原型链中,就可以继承Parent3原型链中的属性

但是这里又增加了一个问题,通过注释我们可以看到Parent3执行了两次,第一次是改变Child3prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,这样将会增加开销,也是一个隐患。

原型式继承

这里主要借助 Object.create() 方法实现普通对象的继承。 传入的参数为生成对象的父类 Object.create() 代码实现如下:

function object(o) { 
  function F() {}; //临时构造函数
  F.prototype = o; //传入对象o作为临时构造函数的原型对象
  return new F(); //返回临时构造对象实例 
}
let parent4 = {
  name: 'parent4',
  friends: ['p1', 'p2', 'p3'],
  getName: function() {
    return this.name
  }
}

let s1 = Object.create(parent4)
let s2 = Object.create(parent4)

s1.name = 'tom'
s1.friends.push('jerry')

console.log(s1.name) // tom
console.log(s1.friends) // (4) ['p1', 'p2', 'p3', 'jerry']
console.log(s2.friends) // (4) ['p1', 'p2', 'p3', 'jerry']

由于 Object.create 方法实现的是浅拷贝,多个实例引用类型指向的是相同的内存

寄生式继承

寄生式继承在上面继承的基础上进行了优化,通过一个函数增加了一些方法

let parent5 = {
  name: 'parent5',
  friends: ['p1', 'p2', 'p3'],
  getName: function() {
    return this.name
  }
}

function clone(o) {
  let clone = Object.create(o)
  clone.getFriends = function () {
    return this.friends
  }
  return clone
}

let s3 = clone(parent5)
let s4 = clone(parent5)

s3.name = 'tom'
s4.friends.push('jerry')

console.log(s3.name) // tom
console.log(s3.friends) // (4) ['p1', 'p2', 'p3', 'jerry']
console.log(s4.friends) // (4) ['p1', 'p2', 'p3', 'jerry']

寄生组合式继承

寄生组合式继承利用寄生继承的核心方法:Object.create() 优化了组合继承会额外执行一次父类构造函数的问题

function Parent6() {
  this.name = 'parent6'
  this.play = [1, 2]
}

Parent6.prototype.getName = function() {
  return this.name
}

function Child6() {
  Parent6.call(this)
  this.age = 18
}

function inheritPrototype (parent, child) {
  child.prototype = Object.create(parent.prototype) // 减少了组合继承中多进行了一次构造
  // child.prototype.constructor = child
}

inheritPrototype(Parent6, Child6)

Child6.prototype.getFriends = function () {
  return this.friends
}

let jarry = new Child6()
let joy = new Child6()
jarry.play.push(4)
console.log(jarry)
console.log(jarry.getName())
console.log(jarry.play) // (3) [1, 2, 4]
console.log(joy.play) // (2) [1, 2]

它利用 Object.create() 创建了父类构造函数原型的(副本)浅复制,并将其赋值给子类构造函数的原型,这样子类的实例对象就可以访问到父类的原型属性和方法,同时它不会额外调用一次父类。

寄生组合继承模式是目前最优的继承方式,其实ES6的 extends 的语法糖本质上也正与这种继承方式基本类似。