图解JS中的六种继承:原型链、盗用构造函数、组合继承、原型式继承、寄生继承、组合寄生继承
继承
继承有两种:接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)。接口继承需要有方法签名,JS由于没有方法签名,因此接下来我们讲解的六种继承都是属于实现继承。
继承本质上就是一个对象复用另一个对象的属性和方法,注意,属性和方法的复用效果是不同的,从这点出发去理解继承,也就能理解为什么会产生六种继承。
原型链 Prototype Chaning
首先说一下原型链:
function Animal(){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.say = function(index){
console.log(`I am ${this.names[index]}`)
}
}
var oneAnimal = new Animal()
oneAnimal.say(1)
每一个方法都有原型,我们可以在原型链挂载属性或者方法,原型在方法和通过new 方法产生的实例之间分享:
function Animal(){
}
Animal.prototype.names = ['animal', 'zoom', 'beast', 'creature']
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
var oneAnimal = new Animal()
oneAnimal.say(1)
原型链最大的好处是实例间可以复用:
var anotherAnimal = new Animal()
anotherAnimal.say(2)
接着说一下原型链是怎么继承的:
function Animal(){
}
Animal.prototype.names = ['animal', 'zoom', 'beast', 'creature']
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
function Cat(){
this.whiskers = 8
}
Cat.prototype = new Animal()
var kitten = new Cat()
kitten.say(2)
原型链有两个大问题,一个是引用值在实例之间互用:
var kitten = new Cat()
var oldCat = new Cat()
kitten.names.push('kitten')
oldCat.say(4)
第二个问题是,在创建实例的时候,不能传参给父类构造器。
盗用构造器 Constructor Stealing
为了解决引用值的问题和无法传参给父类的情况,开发者开始使用称为盗用构造器的方法,也成为对象伪装或者经典继承(masquerading or classic inheritance)。
function Animal(myName){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.names.push(myName)
}
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
function Cat(myName){
Animal.apply(this, [myName])
}
Cat.prototype = new Animal()
var kitten = new Cat('kitten')
kitten.say(4)
盗用构造器是一种很有趣的比喻,它是说,把父类的构造器当成自己的构造器来使用,最重要的目的是做一份属性的副本,也就是拷贝引用值,这样就不用担心实例之间互相修改引用值。同时,在引用了父类构造器的时候传参,也解决了创建实例不能传参给父类的问题。
var kitten = new Cat('kitten')
var oldCat = new Cat('oldCat')
kitten.say(4)
oldCat.say(4)
组合继承 Combination Inheritance
盗用构造器中,当我们继承父类之后,如果要添加方法(or属性)或者重写方法的话,我们自然而然会想到:
function Animal(myName){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.names.push(myName)
}
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
function Cat(myName, age){
Animal.apply(this, [myName])
this.age = age
}
Cat.prototype = new Animal()
Cat.prototype.sayAge = function(){
console.log(`My age = ${this.age}`)
}
var kitten = new Cat('kitten', 3)
kitten.sayAge()
没错,这个就是组合继承,也称为伪经典继承(pseudoclassical inheritance),基本思想是盗用构造器+添加属性+原型链附着方法。
组合继承比盗用构造器更加合理的解决了引用值、传参、添加属性/方法的问题,但实际上,组合继承还不够完美,它还需要两次new Animal() 父类对象。
原型式继承 Prototypal Inheritance
在之前的例子中,我们需要先自定义一个Animal 父类,然后再定义Cat 子类去继承它,原型式继承的作用是跳过第一步,使用一个现成的类直接继承。
function create(fatherInstance){
function Son(){}
Son.prototype = fatherInstance
return new Son()
}
这个就是原型式继承,Object.create() 就是使用这种方式:
In 2006, Douglas Crockford wrote an article, “Prototypal Inheritance in JavaScript”.
寄生继承 Parasitic Inheritance
function parasiticCreate(fatherInstance){
var sonInstance = create(fatherInstance)
sonInstance.sayHi = function(){
console.log(`Hi`)
}
return sonInstance
}
寄生继承核心实现是完成继承+给实例添加方法。
寄生组合继承 Parasitic Combination Inheritance
基础寄生组合继承:
function parasiticCombination(Father, Son){
var middle = create(Father.prototype)
middle.contructor = Son
Son.prototype = middle
}
寄生组合继承:盗用构造器继承属性+寄生继承来创建一个新对象(作为子类对象的新原型)
function Animal(myName){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.names.push(myName)
}
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
function Cat(myName, age){
Animal.apply(this, [myName])
this.age = age
}
parasiticCombination(Animal, Cat)
Cat.prototype.sayAge = function(){
console.log(`My age is ${this.age}`)
}
var kitten = new Cat('kitten', 3)
var oldCat = new Cat('oldCat', 9)
kitten.say(4)
oldCat.say(4)
对比一下组合继承,寄生组合继承中不再去new 一个Animal 实例作为Cat 的原型,而是通过寄生继承产生一个新对象替代,因此少了一次调用父类构造器,减少了不必要的属性。
寄生组合继承是目前ES6类的实现方法,它被认为是性能最好的继承方案。
实际实现:
function Animal(myName){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.names.push(myName)
}
Animal.prototype.say = function(index){
console.log(`I am ${this.names[index]}`)
}
function Cat(myName, age){
Animal.apply(this, [myName])
this.age = age
}
var middle = Object.create(Animal.prototype)
Cat.prototype = middle
Cat.prototype.sayAge = function(){
console.log(`My age is ${this.age}`)
}
var kitten = new Cat('kitten', 3)
var oldCat = new Cat('oldCat', 9)
kitten.say(4)
oldCat.say(4)
转换后:
class Animal{
constructor(myName){
this.names = ['animal', 'zoom', 'beast', 'creature']
this.names.push(myName)
}
say(index){
console.log(`I am ${this.names[index]}`)
}
}
class Cat extends Animal{
constructor(myName, age){
super(myName)
this.age = age
}
sayAge(){
console.log(`My age is ${this.age}`)
}
}
var kitten = new Cat('kitten', 3)
var oldCat = new Cat('oldCat', 9)
kitten.say(4)
kitten.sayAge()
oldCat.say(4)
oldCat.sayAge()
所以为什么说ES6继承是原型链的语法糖,因为背后封装了好几道复杂的工艺。
总结
最后
最后,期待大家能够关注个人公粽号:YopenTech 主要引进国外前沿技术文章、博客,学习国外先进技术,以此师夷长技以制夷,争做一个高质量技术公众号,而不是广告混杂、哗众取宠、猎奇标题、质量低下。
|