谈一谈我对于JS继承的理解

69 阅读5分钟

最初的迷茫

我们知道JS有多种继承方式,其中广为人知的是原型链继承,借用构造函数继承,组合式继承,原型式继承,寄生式继承,寄生组合式继承……

自认为死记硬背可以记住,没成想,面试官一问,脑袋里的知识一团乱麻,一时间竟理不出个头绪。

抓住本质,轻松记住

容我想一下,继承的本质,只不过是要让子类拥有父类的属性和方法。

条条大路通罗马,上面六种继承方式应该就是诸多通往“罗马”的路。

想到这里,干劲就来了,抓住兴奋,继续分析。

再分析

那么为了“继承父类的属性和方法”,我们到底需要做哪几件事呢?

有,一共两件事

  • 继承父类为实例准备的属性和方法
  • 继承父类原型链上的属性和方法

下面代码描述了上面加粗字体引用的代码

// 一 父类的为实例准备的属性和方法
Father(name){
    this.name = name
    this.getName = function(){
        return this.name
    }
}

// 二 父类原型链上的属性和方法
Father.prototype = {
                       favoriteFood: { name: '羊肉' }
                       eat: function(food){ 
                           console.log('阿木阿木, 我吃了:'+food.name)
                       }
                   }

所以说回来,我们所谓的继承,最终的目的就是要完成上面两件事,继承父类的为实例准备的或是父类原型链上的属性和方法

那么我们顺着这条路线去找,第一种继承方式《原型链继承》,是不是都满足了呢?

原型链继承

子类的原型是父类的实例。子类可以访问到父类的所有属性和方法。

Children.prototype = new Father()

这样继承虽然看起来效率很高,但是所有继承的属性和方法都是公有的。

我们知道,很多场景中,比如name属性,即便我从父类中继承,但是我仍希望这个是只属于当前实例的属性。

所以,就有人利用Function.call函数,直接在子类的实例化过程中,利用了父类的构造函数去为实例增加(继承)属性和方法

借用构造函数继承

function Children(name){
    Father.call(this, name)
}

这样做最大好处是弥补了原型链继承的缺少实例私有的属性和方法的问题。但缺点也很明显,无法继承父类的原型。

组合式继承

小孩子才做选择,成年人我全都要

既然选型量继承解决了父类原型继承的问题,借用构造函数继承解决了子类实例的私有属性和方法的继承问题,那为什么不将二者结合起来呢?

function Children(name){
    Father.call(this, name)
}

Children.prototype = new Father()

高手的操作,没有一步是多余的

到此为止,组合式继承已经完美满足了我们的需求。

不过我们要烹饪一道精致的汤菜,多余的步骤,可以优化的地方,一步也不能少!

上面组合式继承中,有甚多问题

  1. 原型链继承,子类继承到的原型不够纯净。我们只想要父类的原型上的属性和方法,而该方法意外的继承了父类为实例准备的一些属性和方法。
  2. 组合式继承中,我们其实一共调用了两次父类的构造函数

原型式继承&寄生式继承

是原型链继承的升级!解决了子类继承的原型不够纯净的问题

原型式继承

与原型链继承最大的区别,就是原型式继承更像是一个工厂函数,强调继承的是一个链有父类原型的对象,而非仅是父类的实例。

怎么理解呢,就是这个对象可以是一个prototype,也可以是一个实例,更可以是一个带着父类prototype的空对象。

var proto = Father.prototype

function createObj(proto){
    function fn(){}
    fn.prototype = proto
    return new fn()
}

var child = createObj(proto)

如果把上面的代码封装成函数,那么会得到一个类似Object.create()的方法。但它的问题也是显而易见的,缺少继承私有属性和方法的能力。

寄生式继承

可以看做是原型继承的升级版,对于创建出来的实例,多做了一些对象增强的事情,不过依然不能继承父类为实例准备的私有属性和方法。

var proto = Father.prototype

function createObj(proto){
    var obj = Object.create(proto)
    // 对象增强
    obj.getName = function(){}
    return obj
}
var child = createObj(proto)

继承最终章

我们依然采用组合的方式来封装我们的继承方法。

寄生组合式继承

这时候,寄生式继承只负责将链有父类原型的对象赋值给子类的原型,搭配一些对象增强的操作,完成子类对于父类原型的继承。

接下来,通过借用构造函数的模式,在子类实例化时调用父类的构造函数,完成对于父类为实例准备的属性和方法的继承。

全程只调用一次父类的构造函数

var proto = Father.prototype

// 继承父类的原型
function prototype(_c, proto){
    // 寄生式继承 - start
    var obj = Object.create(proto)
    obj.getName = function(){}
    // 寄生式继承 - end
    
    _c.prototype = obj
    obj.constructer = _c
   
}
// 子类
funciton Children(){
    Father.call(this)
}
// 继承父类的原型
prototype(Children, proto)

// 实例化
var child = new Children()

结尾

最后,我想说,寄生组合式继承真的是很理想化的JS继承范式!体现在以下三点

  1. 高效率:它只调用了一次 Parent 构造函数
  2. 无副作用:避免了在 Parent.prototype 上面创建不必要的、多余的属性。
  3. 原型链还能保持不变:能够正常使用 instanceof 和 isPrototypeOf。

参考文章: 深入JS系列-继承:github.com/mqyqingfeng… 《JavaScript高级程序设计》-继承章节