原型链继承
原型链继承是比较常见的继承方式之一,其中涉及构造函数、原型和实例,三者之间存在一定的关系,每一个构造函数都有一个原型对象,即 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 的原本的属性外也继承了 Parent2 的 name 属性,但是只是解决了第一种继承方式的弊端,拥有了自己的内存空间,但是无法继承父类所属的原型链中的属性方法,只能继承父类实例中的属性和方法, hasownproperty 为 true 的属性
组合继承
这种方法结合了前两种继承方式,代码如下:
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执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 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 的语法糖本质上也正与这种继承方式基本类似。