JS中的继承

355 阅读6分钟

前言

断更有段时间了,趁着最近在看继承方面的东西,整理一些相关的内容,方便日后回顾

正文

什么是继承

继承,也就是面向对象的继承,继承封装多态是面向对象的 三个基本特征。

通过继承,可以使子类具有父类的一些属性和方法。其他语言的继承行为,就不做说明和记录,后文提及的继承均指JS中的继承行为。

继承机制

JS中继承无非就是基于两种方式

  • 构造函数
  • 原型链

具体如何通过这两种方式实现了继承,可以通过简单的代码做以说明

对下文代码中使用到的类及实例的说明

Cat:猫这类动物,特点是会喵喵喵的叫,世界上的猫都有同一个名字—咪咪

Orange:猫下面的一类猫,特点是颜色带点橘,俗称橘猫

漙漙:橘猫中的一个具体的个体,也就是她 👇,我给她取了个别名叫漙漙

构造函数

即借用构造函数,子类通过apply、call来调用父类的构造函数

场景:子类仅需要独享父类的自身属性

function Cat(){                     
    this.voice = 'miaomiaomiao'     
} 
Cat.prototype.name = 'mimi'                                  
function Orange(){                  
    Cat.call(this)                  
    this.color = 'orange'           
}                                   
const tuantuan = new Orange()       
tuantuan    //    { voice: 'miaomiao', color: 'orange' }
tuantuan.__protot__    //    {constructor: ƒ Orange() }

 

通过这种方式继承的对象不包含父类的原型属性,因为整个过程中并没有调用 new 产生实例,相当子类将父类的独享属性拷贝了一份到自己内存中。其后对子类做任何修改都不会影响到父类。

原型链

即修改子类的原型对象,将子类的原型指向父类的实例

场景:子类可以与其他子类共享父类的自身属性和prototype上的属性

function Cat(){                 
    this.voice = 'miaomiaomiao' 
}     
Cat.prototype.name = 'mimi'                          
function Orange(){              
    this.color = 'orange'       
}                               
Orange.prototype = new Cat()    
const tuantuan = new Orange()   
tuantuan    // { color: 'orange' }
tuantuan.voice  // 'miaomiao'   
tuantuan.name   // 'mimi'
tuantuan.__proto__    //    Cat { voice:  'miaomiaomiao' }

 

这里要做一个特别的说明,当尝试去修改类的prototype时,需要意识到,我们在修改了prototype的指向后,同时也修改了类的prototype.constructor的指向,因此可能会带来一些潜在的问题,最好的方案是在修改完prototype的指向后重新指定constructor

Orange.prototype.constructor = Orange

 

两种继承方式的区别,或者优缺点,无非就是属性的独享与共享的区别,这里就不多说,下文总结基于这两种方式的继承方案时再去具体讨论

关于场景中独享、共享,也可以简单粗暴的理解为是否需要传参

常见的继承方案

组合继承

组合继承,名字就能看出来,将上述两种方式的优点结合的一种继承方案。

场景:一般用于子类既需要独享父类的自身属性,同时需要和其他子类共享父类的prototype上的属性

 

function Cat(){
    this.voice = 'miaomiaomiao'
}
Cat.prototype.name = 'mimi'
function Orange(){
    Cat.call(this)
    this.color = 'orange'
}
Orange.prototype = new Cat()
const tuantuan = new Orange()
tuantuan    //  { voice: 'miaomiaomiao', color: 'orange' }
tunatuan.name   // 'mimi'
tuantuan.__proto__    //    Cat { voice:  'miaomiaomiao' }

通过这种继承方式生成的实例既可以独享父类的属性和方法(通过借用构造函数将父类的属性复制一份到自己的内存中,成为子类独有的属性,调用构造函数时可传参定制),同时又能与其他子类共享父类的属性(即父类prototype中的属性,所有子类通过内存地址共同引用这个属性或方法)

这种方式有一个很明显的缺陷,就是会调用两次父类的构造函数,一次发生在call方法继承其独享(自身)属性时,另一次则发生在通过 new 继承原型属性时。

重复调用的结果就是子类的实例,不但拷贝了一份父类的自身属性到自己内存中,又在自己的 __proto__ 中生成了父类的自身属性,可以看到上面的例子,tuantuantuantuan.__proto__中都有自己的 voice 属性。

原型式继承

这种继承,可以看成是对组合式继承的一种改造和封装。通过将父类的自身属性和共享属性都挂在了子类的prototype上,消除了调用两次父类构造函数造成的问题。

场景:子类需要继承父类的自身属性和共享属性,但不要求独享父类的自身属性

function Cat(){
    this.voice = 'miaomiaomiao'
    this.sayHi = function(){
        console.log('miao~')
    }
}
Cat.prototype.name = 'mimi'
function creatOrange(obj){
    function Orange(){
        this.color = 'orange'
    }
    Orange.prototype = obj
    return new Orange()
}
const tuantuan = creatOrange(new Cat())
tuantuan    //  { color: 'orange' }
tuantuan.__proto__  //  {voice: "miaomiaomiao", sayHi: ƒ}

通过将父对象的实例对象设置成子对象的原型构建继承关系,这种方式也是ES5 Object.create()的原理

function create(o){
    function F(){}
    F.prototype = o
     return new F()
}

寄生式继承

寄生式继承,对原型式继承的一种定制化封装,可以理解为升级版的工厂模式,即在内部用某种方式对生成的对象进行拓展和增强。

场景:子类需要继承父类的自身属性和共享属性,但不要求独享父类的自身属性,同时对生成的实例需要做统一定制化处理。

比如给我的橘猫以及阿黄(父类是Dog)都要戴一个订制的牌子,上面是我的联系方式

function Cat(){
    this.voice = 'miaomiaomiao'
    this.sayHi = function(){
        console.log('miao~')
    }
}
Cat.prototype.name = 'mimi'
function MyOrangeCat(obj){
    function creatOrange(obj){
        function Orange(){
            this.color = 'orange'
        }
        Orange.prototype = obj
        return new Orange()
    }
    let myOrangeCat = creatOrange(obj)
    myOrangeCat.tag = {
        master: 'Jonas',
        qq: 287112776
    }
    return myOrangeCat
}
const tuantuan = MyOrangeCat(new Cat())
tuantuan    //    Orange {color: "orange", tag: {…}}
tuantuan.__proto__    //Cat {voice: "miaomiaomiao", sayHi: ƒ}

 

形式上,和上一个原型式继承很像,将原型式继承做了一层包装,将原型式继承返回的对象做一层加强,然后再返回。

寄生组合式继承

该种继承方式重点在于改造组合式继承,解决调用两次父类构造函数的问题。

组合式继承中通过 call 调用构造函数这一步,是不可或缺的一步,但是设置子类的 prototype 这一步,可以做一个优化,因为这里我们需要的,仅仅是父类 prototype 的一个副本

function Cat(){
    this.voice = 'miaomiaomiao'
    this.sayHi = function(){
        console.log('miao~')
    }
}
Cat.prototype.name = 'mimi'
function Orange(){
    this.color = 'orange'
}
function inheritPrototype(subType, superType){
    let prototype = Object.create(superType.prototype)
    subType.prototype = prototype
    prototype.constructor = subType
}
inheritPrototype(Orange, Cat)
const tuantuan = new Orange()

 

这里为什么不直接通过原型继承父类的prototype

Orange.prototype = Cat.prototype

 

 这种方式修改Orange.prototype会导致Cat的prototype也被修改,,之后的对 prototype.voice和 prototype.sayHi的操作均会影响到Cat的其他子类,所以这里需要通过Object.crate()创建一个父类prototype的副本。

但是这种方式为何叫寄生组合式继承?从命名来看,初次的理解是为满足组合式继承和继承式继承需求的并集而设计的一种解决方案,实际上并非如此。

 

以上这些与其叫继承方式,倒不如看成是工具函数的封装。每种方式都有自己的应用场景,不同需求使用不同方式,并无优劣之分,本质上都是通过原型链或者构造函数实现继承,重点在于如何快速分析提炼自己的业务需求,选用最佳的解决方案解决痛点。