1.原型链继承
基于原型链查找的特点,我们将父类的实例作为子类的原型,这种继承方式便是原型链继承。
function Parent() {
this.color = 'red'
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log('初始状态')
}
function Child () {}
Child.prototype = new Parent() // constructor指针变了 指向了Parent
Child.prototype.constructor = Child // 手动修复
let child = new Child()
Child.prototype相当于是父类Parent的实例,父类Parent的实例属性被挂到了子类的原型对象上面,拿color属性举个例子,相当于就是这样
Child.prototype.color = 'red'
这样父类的实例属性都被共享了,我们打印一下child,可以看到child没有自己的实例属性,它访问的是它的原型对象。
我们创建两个实例child1,child2
let child1 = new Child()
let child2 = new Child()
child1.color = 'blue'
console.log(child1,child2)
我们修改了child1的color属性,child2没有受到影响,并非是其它实例拥有独立的color属性,而是因为这个color属性直接添加到了child1上面,它原型上的color并没有动,所以其它实例不会受到影响从打印结果也可以清楚看到这一点。那如果我们修改的属性是个引用类型呢?
let child1 = new Child()
let child2 = new Child()
child1.queue = [1,2,3,4]
child1.like = function () {
console.log('我被修改了')
}
console.log(child1,child2)
我们重写了引用类型的queue属性和like方法,其实和修改color属性是完全一样的,它们都直接添加到了child1的实例属性上。从打印结果能看到这两个属性已经添加到了child1上了,而child2并不会受到影响,再来看下面这个。
let child1 = new Child()
let child2 = new Child()
child1.queue.push(4) // 这次没有重新赋值
console.log(child1,child2)
如果进行了重新赋值,会添加到到实例属性上,和原型上到同名属性便无关了,所以并不会影响到原型。这次我们采用push方法,没有开辟新空间,修改的就是原型。child2的queue属性变化了,子类Child原型上的queue属性被实例修改,这样肯定就影响到了所有实例。
缺点:
1.子类的实例会共享父类构造函数引用类型的属性
2.创建子类实例的时候无法传参
2.构造函数式继承
相当于拷贝父类的实例属性给子类,增强子类构造函数的能力
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`I like ${this.name}`)
}
function Child (name) {
Parent.call(this, name)
}
let child = new Child('xxx')
console.log(child)
我们打印了一下child,可以看到子类拥有父类的实例属性和方法,但是child的proto上面没有父类的原型对象。解决了原型链的两个问题(子类实例的各个属性相互独立、还能传参)
缺点:
1.子类无法继承父类原型上面的方法和属性。
2.在构造函数中定义的方法,每次创建实例都会再创建一遍。
3.组合继承
组合继承便是把上面两种继承方式进行组合。
function Parent(name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`I like ${this.name}`)
}
function Child(name) {
Parent.call(this, name) //通过构造函数继承方法子类得以继承构造函数的实例属性
}
Child.prototype = new Parent() //通过原型链继承方法让子类继承父类构造函数原型上的方法
Child.prototype.constructor = Child
let child1 = new Child('xxx')
let child2 = new Child('ooo')
child1.queue.push(4)
console.log(child1,child2)
我们更新了child1的引用属性,发现child2实例没受到影响,原型上的like方法也在,不错,组合继承确实将二者的优点发扬光大了,解决了二者的缺点。组合模式下,通常在构造函数上定义实例属性,在原型对象上定义要共享的方法,通过原型链继承方法让子类继承父类构造函数原型上的方法,通过构造函数继承方法子类得以继承构造函数的实例属性,是一种功能上较完美的继承方式。
缺点:父类构造函数被调用了两次,第一次调用后,子类的原型上拥有了父类的实例属性,第二次call调用复制了一份父类的实例属性作为子类Child的实例属性,那么子类原型上的同名属性就被覆盖了。虽然被覆盖了功能上没什么大问题,但这份多余的同名属性一直存在子类原型上,如果我们删除实例上的这个属性,实际上还能访问到,此时获取到的是它原型上的属性。
Child.prototype = new Parent() // 第一次构建原型链
Parent.call(this, name) // 第二次new操作符内部通过call也执行了一次父类构造函数
4.原型式继承
将一个对象作为基础,经过处理得到一个新对象,这个新对象会将原来那个对象作为原型,这种继承方式便是原型式继承,一句话总结就是将传入的对象作为要创建的新对象的原型。
function prodObject (obj) {
function F () {}
F.prototype = obj
return new F()
} // Object.create()的实现原理
let base = {
name: 'xxx',
queue: [1,2,3]
}
let child1 = prodObject(base)
let child2 = prodObject(base)
console.log(child1, child2)
原型式继承基于prototype,和原型链继承类似,这种继承方式下实例没有自己的属性值,访问到也是原型上的属性。
缺点:同原型链继承
5.寄生式继承
原型式继承的升级,寄生继承封装了一个函数,在内部增强了原型式继承产生的对象。
function prodObject(obj) {
function F () {
}
F.prototype = obj
return new F()
}
function greaterObject(obj) {
let clone = prodObject(obj)
clone.queue = [1,2,3]
clone.like = function() {}
return clone
}
let parent = {
name:'xxx',
color: ['red', 'blue']
}
let child = greaterObject(parent)
console.log(child)
它的缺点也很明显了,寄生式继承增强了对象,却也无法避免原型链继承的问题。
缺点:
1.拥有原型链继承的缺点
2.除此,内部的函数无法复用
6.寄生组合式继承
上面说到,组合继承的问题在于会调用二次父类,造成子类原型上产生多余的同名属性。Child.prototype = new Parent(),那这行代码该怎么改造呢?
我们的目的是要让父类的实例属性不出现在子类原型上,如果让Child.prototype = Parent.prototype,这样不就能保证子类只挂载父类原型上的方法,实例属性不就没了吗,代码如下,看起来好像是简直不要太妙啊。
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`like${this.name}`)
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = Parent.prototype // 只改写了这一行
Child.prototype.constructor = Child
let child = new Child('')
console.log(child)
回过神突然发现改写的那一行如果Child.prototype改变了,那岂不是直接影响到了父类,举个栗子
function Parent (name) {
this.name = name
this.queue = [1,2,3]
}
Parent.prototype.like = function () {
console.log(`like${this.name}`)
}
function Child (name) {
Parent.call(this, name)
}
Child.prototype = Parent.prototype // 只改写了这一行
Child.prototype.constructor = Child
Child.prototype.addByChild = function () {}
Parent.prototype.hasOwnProperty('addByChild')
let child = new Child('')
console.log(child, new Parent())
addByChild方法也被加到了父类的原型上,所以这种方法不够优雅。同样还是那一行,直接访问到Parent.prototype存在问题,那我们可以产生一个以Parent.prototype作为原型的新对象,这不就是上面原型式继承的处理函数prodObject吗
Child.prototype = Object.create(Parent.prototype) // 改为这样
这样就解决了所有问题,我们怕改写Child.prototype影响父类,通过Object.create返回的实例对象,我们将Child.prototype间接指向Parent.prototype,当再增加addByChild方法时,属性就和父类没关系了。
寄生组合式继承也被认为是最完美的继承方式,最推荐使用。
总结
js的继承方式主要就这六种,es6的继承是个语法糖,本质也是基于寄生组合。这六种继承方式,其中原型链继承和构造函数继承最为基础和经典,组合继承聚合了它们二者的能力,但在某些情况下会造成错误。原型式继承和原型链相似,寄生式继承是在原型式继承基础上变化而来,它增强了原型式继承的能力。最后的寄生组合继承解决了组合继承的问题,是一种最为理想的继承方式。