图解JS中的六种继承:原型链、盗用构造函数、组合继承、原型式继承、寄生继承、组合寄生继承

·  阅读 631
图解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) // I am zoom
复制代码

image.png 每一个方法都有原型,我们可以在原型链挂载属性或者方法,原型在方法和通过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) 
//> I am zoom
复制代码

image.png

原型链最大的好处是实例间可以复用:

var anotherAnimal = new Animal()
anotherAnimal.say(2)
//> I am beast
复制代码

image.png

接着说一下原型链是怎么继承的:

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
}

// 从Animal中继承
Cat.prototype = new Animal()

var kitten = new Cat()
kitten.say(2) //> I am beast
复制代码

image.png

原型链有两个大问题,一个是引用值在实例之间互用:

var kitten = new Cat()
var oldCat = new Cat()
kitten.names.push('kitten')
oldCat.say(4) //> I am kitten
复制代码

第二个问题是,在创建实例的时候,不能传参给父类构造器。

盗用构造器 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) //> I am kitten
复制代码

image.png

盗用构造器是一种很有趣的比喻,它是说,把父类的构造器当成自己的构造器来使用,最重要的目的是做一份属性的副本,也就是拷贝引用值,这样就不用担心实例之间互相修改引用值。同时,在引用了父类构造器的时候传参,也解决了创建实例不能传参给父类的问题。

var kitten = new Cat('kitten')
var oldCat = new Cat('oldCat')
kitten.say(4) //> I am kitten
oldCat.say(4) //> I am oldCat
复制代码

组合继承 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()
//> My age = 3
复制代码

没错,这个就是组合继承,也称为伪经典继承(pseudoclassical inheritance),基本思想是盗用构造器+添加属性+原型链附着方法

image.png

组合继承比盗用构造器更加合理的解决了引用值、传参、添加属性/方法的问题,但实际上,组合继承还不够完美,它还需要两次new Animal()父类对象。

原型式继承 Prototypal Inheritance

在之前的例子中,我们需要先自定义一个Animal父类,然后再定义Cat子类去继承它,原型式继承的作用是跳过第一步,使用一个现成的类直接继承。

function create(fatherInstance){
    function Son(){}
    Son.prototype = fatherInstance
    return new Son()
}
复制代码

image.png

这个就是原型式继承,Object.create()就是使用这种方式:

image.png

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
}
复制代码

image.png

寄生继承核心实现是完成继承+给实例添加方法。

寄生组合继承 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)
复制代码

image.png

对比一下组合继承,寄生组合继承中不再去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) // 在派生类中,必须调用super(),否则会报错
        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()
//> I am kitten
//> My age is 3
//> I am oldCat
//> My age is 9
复制代码

所以为什么说ES6继承是原型链的语法糖,因为背后封装了好几道复杂的工艺。

总结

image.png

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改