详解js中的继承(二)

前言

趁周末结束以前赶忙先把坑填上。上回咱们说到了原型链,而且留下了几个思考题,先把答案公布一下。数组

  1. 在最后一个例子里,console.log(b1.constructor),结果是什么?
    答案:function A,由于b1自己没有constructor属性,会沿着原型链向上找到B prototype对象,而后再往上找到A prototype对象,此时找到了constructor属性,也就是指向函数对象A,可参见上文最后一张图片app

  2. B.prototype = new A();B.prototype.sayB = function(){ console.log("from B") }这两句的执行顺序能不能交换?
    答案:不能,由于咱们说过了,第一句是把改写B函数对象的prototype指向的原型对象,若是咱们交换了顺序,是在原先的B的原型对象上绑定了方法,而后再把指针指向新的原型对象,那新的原型对象上天然就没有绑定sayB方法,接下来的b1.sayB()就会报函数未定义错误,函数

  3. 在最后一个例子里,A看似已是原型链的最顶层,那A还能再往上吗?
    答案,能够,由于其实全部的引用类型都默认继承了了Object,也就是说,完整的原型链应该是A prototype[Prototype]属性指向Object prototype。如图:学习

完整的原型链
顺便补充一下,Object prototype上的原生方法,包括咱们经常使用的hasOwnProperty()isPropertyOf()等。this

接着谈继承

在上一篇咱们讲解了原型链的原理,建议没有理解清楚的读者朋友先理解以前的知识点,避免难点叠加spa

原型链的缺陷

  1. 引用类型的值在原型链传递中存在的问题
    咱们知道js中有值类型和引用类型,其中引用类型包括Object.Array等,引用类型的值有一个特色:在赋值的时候,赋给变量的是它在内存中的地址。换句话说,被赋值完的变量至关于一个指针,这会有什么问题呢?看例子:prototype

    function A() {
            this.name = "a" 
            this.color = ['red','green'];         
        }
        function B(){
    
        }
         //让B的原型对象指向A的一个实例
         B.prototype = new A();
         
         //生成两个个B的实例
         var b1 = new B();
         var b2 = new B();
         //观察color属性
         console.log(b1.name)//a
         console.log(b2.name)//a
         console.log(b1.color)//[red,green]
         console.log(b2.color)//[red,green]
         //改变b1的name和color属性
         b1.name = 'b'
         b1.color.push('black')
         
         //从新观察color属性
         console.log(b1)//b
         console.log(b2)//a
         console.log(b2.name)
         console.log(b1.color)//["red", "green", "black"]
         console.log(b2.color)//["red", "green", "black"]

    发现问题了吗?咱们修改了b1的color和name属性,可是b2name属性不变,color属性发生了改变。为了搞清楚这里问题,请尝试回答个人问题(想不出来的话,能够本身经过在控制台打印出来验证):指针

    1. b1b2有本身的color属性吗?
      答案:没有,只是B prototype上有color属性,由于它是A的一个实例,b1b2实际上是经过[Proto]属性访问B prototype上的color属性(指针),从而访问和操做color数组的;code

    2. b1b2有本身的name属性吗?
      答案:一开始都没有,当执行了b1.name = 'b'时,至关于b1有了本身的name属性,而b2依然没有name属性。orm

    因此以上问题的缘由来源就是咱们前面说的:引用类型的值在赋值的时候,赋给变量的是它在内存中的地址。(若是关于值类型和引用类型有没掌握的同窗能够先去看看或者私下问我,这里默认这个是已经了解的。)
    因此在原型链中若是A(其实就是继承中的父类型)含有引用类型的值,那么子类型的实例共享这个引用类型得值,也就是上面的color数组,这就是原型链的第一个缺陷。

  2. 第二个缺陷是:在建立子类型的实例(如b1,b2)时,没法向父类型的构造函数中传递参数。好比在上面的例子中,若是Aname属性是要传递参数的而不是写死的,那么咱们在实例化b1b2的时候根本无法传参

借用构造函数继承

为了解决引用类型值带来的问题,咱们会采用借用构造函数继承的方式,又名*伪造对象或者经典继承,核心思路是:咱们在子类型的构造函数中调用父类型的构造函数,这里要用到一个方法call()或者apply()函数,关于这个函数,我这里简单介绍一下,能够简单的理解功能就是,容许一个对象调用另外一个对象的方法。具体的做用若是你们以为须要能够在评论区回复,我会后面单独写一下这两个函数。在这里就不展开了。具体实现以下:

function A() {
            this.name = "a" 
            this.color = ['red','green'];         
        }
        function B(){
          //“借用”|就体如今这里,子类型B借用了父类型A的构造函数,从而在这里实现了继承
          A.call(this);
        }
       
         
         //生成两个个B的实例
         var b1 = new B();
         var b2 = new B();
         //观察color属性
         console.log(b1.name)//a
         console.log(b2.name)//a
         console.log(b1.color)//['red','green']
         console.log(b2.color)//['red','green']
         //改变b1的name和color属性
         b1.name = 'b'
         b1.color.push('black')
         
         //从新观察属性
         console.log(b1.name)//b
         console.log(b2.name)//a
         console.log(b1.color)//['red','green','black']
         console.log(b2.color)//["red", "green"]

在这里咱们没有采用原型链,而是利用call()方法来实现在子类型的构造函数中借用父类型的构造函数,完成了继承,这样继承的结果就是:b1,b2都分别拥有本身的namecolor属性(能够直接console.log(b1)查看对象的属性),也就是b1b2彻底独立的。这就解决了以前的第一个问题,并且传递参数的问题其实也能够解决,再稍微改一下A函数:

//这里name改为传递参数的
        function A(name) {
            this.name = name 
            this.color = ['red','green'];         
        }
        function B(name){
          //在这里咱们接受一个参数,而且经过call方法传递到A的构造函数中
          A.call(this,name);
        }
       
         
         //生成两个个B的实例
         var b1 = new B('Mike');
         var b2 = new B('Bob');
         //观察属性
         console.log(b1.name)//Mike
         console.log(b2.name)//Bob
         console.log(b1.color)//['red','green']
         console.log(b2.color)//['red','green']

其实上面就能够直接写成这样,可是为了让你们更容易理解,故意分开,隔离变量(你们看我这么用心真的不考虑点个赞吗?),顺便再解释一下A.call(this,name);就是让this对象(这里是指B)调用构造函数A,同时传入一个参数name

能够看到,借用构造函数继承不会有原型链继承的问题,那为何不都借用采用构造函数继承的方法呢?缘由在于:这种继承方式,全部的属性和方法都要在构造函数中定义,好比咱们这里也要绑定以前的sayA()方法并继承,就只能写在A的构造函数里面,而写在A prototype的的方法,无法经过这种方式继承,而把全部的属性和方法都要在构造函数中定义的话,就不能对函数方法进行复用.

组合继承

学习了原型链的继承和借用构造函数的继承后,咱们能够发现,这两种方法的优缺点恰好互补:

  • 原型链继承能够把方法定义在原型上,从而复用方法

  • 借用构造函数继承法能够解决引用类型值的继承问题和传递参数问题

所以,就天然而然的想到,结合这两种方法,因而就有了下面的组合继承,也叫伪经典继承,(前面的借用构造函数是经典继承,能够联系起来),具体实现以下:

function A(name) {
            this.name = name 
            this.color = ['red','green'];     
        }
        A.prototype.sayA = function(){
          console.log("form A")
        }
        function B(name,age){
          //借用构造函数继承
          A.call(this,name);
          this.age = age;
        }

        //原型链
        B.prototype = new A();
        B.prototype.sayB = function(){
          console.log("form B")
        }
         
         //生成两个个B的实例
         var b1 = new B('Mike',12);
         var b2 = new B('Bob',13);
         //观察color属性
         console.log(b1)//{name:'Mike'...}
         console.log(b2)//{name:'Bob'...}
         b1.sayA()//from A
         b2.sayB()//from B

这个例子只是对上面的例子稍做修改:

  1. 咱们在A prototype上定义了sayA() ,在B prototype 定义了sayB()

  2. 咱们增长了B.prototype = new A();原型链

最终实现的效果就是,b1和b2都有各自的属性,同时方法都定义在两个原型对象上,这就达到了咱们的目的:属性独立,方法复用,这种继承的理解相对简单,由于就是把前两种继承方式简单的结合一下,原型链负责原型对象上的方法,call借用构造函数负责让子类型拥有各自的属性。
组合继承是js中最经常使用的继承方式

原型式继承

原型式继承与以前的继承方式不太相同,原理上至关于对对象进行一次浅复制,浅复制简单的说就是:把父对像的属性,所有拷贝给子对象。可是咱们前面说到,因为引用类型值的赋值特色,因此属性若是是引用类型的值,拷贝过去的也仅仅是个指针,拷贝完后父子对象的指针是指向同一个引用类型的(关于深复制和浅复制若是须要细讲的一样能够在评论区留言。)原型式继承目前能够经过Object.create()方式来实现,(这个函数的原理我不想在这里提,由于我但愿读者在看完这里内容之后本身去查阅一下这个内容)本文只讲实现方式:
Object.create()接收两个参数:

  • 第一个参数是做为新对象的原型的对象

  • 第二个参数是定义为新对象增长额外属性的对象(这个是可选属性)

  • 若是没有传递第二个参数的话,就至关于直接运行object()方法(这个方法若是不懂直接百度就好)
    上面的说法可能有点拗口,换句话说:

好比说咱们如今要建立一个新对象B,那么要先传入第一个参数对象A,这个A将被做为B prototype;而后能够再传入一个参数对象CC对象中能够定义咱们须要的一些额外的属性。来看例子

var A  = {
        name:'A',
        color:['red','green']
    }

    //使用Object.create方法先复制一个对象
    var B = Object.create(A);
    B.name = 'B';
    B.color.push('black');

    //使用Object.create方法再复制一个对象
    var C = Object.create(A);
    C.name = 'C';
    B.color.push('blue');
    console.log(A.name)//A
    console.log(B.name)//B
    console.log(C.name)//C
    console.log(A.color)//["red", "green", "black", "blue"]

在这个例子中,咱们只传入第一个参数,因此BC都是对A浅复制的结果,因为name是值类型的,color是引用类型的,因此ABC的name值独立,color属性指向同一个对象。接下来举个传递两个参数的例子:

var A  = {
        name:'A',
        color:['red','green'],
        sayA:function(){
            console.log('from A');
        }
    };

    //使用Object.create方法先复制一个对象
    var B = Object.create(A,{
        name:{
          value:'B'
        }
    });
    console.log(B)//Object{name:'B'}
    B.sayA()//'from A'

这个例子就很清楚的代表了这个函数的做用了,传入的A对象被当作B的原型,因此生成B对象没有sayA()方法,却能够调用该方法(相似于经过原型链),同时咱们在第二个参数中修改了B本身的name,因此就实现了这种原型式继承。原型式继承的好处是:若是咱们只是简单的想保持一个对象和另外一个对象相似,没必要大费周章写一堆代码,直接调用就能实现

寄生式继承

寄生式继承和原型继承联系紧密,思路相似于工厂模式,即建立一个只负责封装继承过程的函数,在函数中根据须要加强对象,最后返回对象

function createA(name){
    //建立新对象
    var obj = Object(name);
    //加强功能
     obj.sayO = function(){
         console.log("from O")
     };
    //返回对象
    return obj;
     
}
var A = {
    name:'A',
    color:['red','green','blue']
};
//实现继承
var  B = createA(A);
console.log(B)//Object {name: "A", color: Array[3]}
B.sayO();//from O

继承的结果是B拥有A的全部属性和方法,并且具备本身的sayO()方法,效果和原型式继承很类似,读者能够比较一下寄生式继承和原型式继承的类似和区别。

寄生组合式继承

终于写到最后一个继承了,咱们在以前讲了5种继承方式,分别是原型链借用构造函数继承组合继承原型式继承寄生式继承,其中,前三种联系比较紧密,后面两种也比较紧密,而咱们要讲的最后一种,是和组合继承还有寄生式继承有关系的。(看名字就知道了嘛)

友情提示:若是看到这里有点累的读者能够先休息一下,由于虽然已经分了一二两篇,本文的篇幅仍是稍长(我都打了两个多小时了),并且若是先把以前的理解清楚,比较容易理解最后一种继承。

组合继承仍有缺陷

咱们在以前说过,最经常使用的继承方式就是组合继承,可是看似完美的组合继承依然有缺点:子类型会两次调用父类型的构造函数,一次是在子类型的构造函数里,另外一次是在实现原型链的步骤,来看以前的代码:

function A(name) {
            this.name = name 
            this.color = ['red','green'];     
        }
        A.prototype.sayA = function(){
          console.log("form A")
        }
        function B(name,age){
         //第二次调用了A
          A.call(this,name);
          this.age = age;
        }

        //第一次调用了A
        B.prototype = new A();
        B.prototype.sayB = function(){
          console.log("form B")
        }
         

         var b1 = new B('Mike',12);
         var b2 = new B('Bob',13);
          console.log(B.prototype)//A {name: undefined, color: Array[2]}

在第一次调用的时候,生成了B.prototype对象,它具备namecolor属性,由于它是A的一个实例;第二次调用的时候,就是实例化b1b2的时候,这时候b1b2也具备了namecolor属性,咱们以前说过,原型链的意义是:当对象自己不存在某个属性或方法的时候,能够沿着原型链向上查找,若是对象自身已经有某种属性或者方法,就访问自身的,可是咱们如今发现,经过组合继承,只要是A里面原有的属性,B prototype对象必定会有,b1b2确定也会有,这样就形成了一种浪费:B prototyope上的属性其实咱们根本用不上,为了解决这个问题,咱们采用寄生组合式继承。
寄生组合式继承的核心思路是其实就是换一种方式实现 B.prototype = new A();从而避免两次调用父类型的构造函数,官方定义是:使用寄生式继承来继承父类型的原型,而后将结果指定给子类型的原型,。`这句话不容易理解,来看例子:

//咱们一直默认A是父类型,B是子类型
function inheritPrototype(B,A){
    //复制一个A的原型对象
    var pro  = Object(A.prototype);
    //改写这个原型对象的constructor指针指向B
    pro.constructor = B;
    //改写B的prototype指针指向这个原型对象
    B.prototype = pro;
}

这个函数很简短,只有三行,函数内部发生的事情是:咱们复制一个A的原型对象,而后把这个原型对象替换掉B的原型对象。为何说这样就代替了 B.prototype = new A();,不妨思考一下,咱们最初为何要把B的prototype属性指向A的一个实例?无非就是想获得A的prototype的一个复制品,而后实现原型链。而如今咱们这样的作法,一样达到了咱们的母的目的,并且,此时B的原型对象上不会再有A的属性了,由于它不是A的实例。所以,只要把将上面的 B.prototype = new A();,替换成inheritPrototype(B,A),就完成了寄生组合式继承。

寄生组合式继承保持了组合继承的优势,又避开了组合继承会有无用属性的缺陷,被认为是最理想的继承方式。

小结

终于写完了!! 明天还得起早去上班,下一次更新可能会放在这一周的周末。关于这一篇内容,建议的阅读方式是先读前三种继承方式,再看后两种继承,都理解的差很少了,就能够看最后一种继承方式了。中间注意消化和休息。最后再提一下吧:若是喜欢本文,请大方的点一下右上角的推荐和收藏(反正大家仍是喜欢只收藏不推荐),虽说写这个一方面是为了本身巩固知识,可是为了让读者更容易理解,我尽可能都是采用拆解的方式来说,并且穿插了新知识的时候都会给出解释,并非直接搬运书本知识过来,那样毫无心义。这么作仍是但愿写的文章可以更有价值,让更多人可以获得帮助!以上内容属于我的看法,若是有不一样意见,欢迎指出和探讨。请尊重做者的版权,转载请注明出处,如做商用,请与做者联系,感谢!

相关文章
相关标签/搜索