JavaScript由浅及深敲开原型链(二)

作者: bb7bb | 来源:发表于2018-03-31 17:19 被阅读112次

    一、对象的继承

    1.了解原型链

    在上一篇我们讲过关于原型对象的概念,当然如果不了解的建议去翻看第一篇文章,文末附有连接。我们知道每个对象都有各自的原型对象,那么当我们把一个对象的实例当做另外一个对象的原型对象。。这样这个对象就拥有了另外一个引用类型的所有方法与属性,当我们再把该对象的实例赋予另一个原型对象时,这样又把这些方法继承下去。如此层层递进,对象与原型间存在链接关系,这样就构成了原型链

    function Animal(){
        this.type = "Animal";
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        this.vioce = "喵喵喵";
    }
    
    Cat.prototype = new Animal();
    
    Cat.prototype.shout = function(){
        console.log(this.vioce);
    }
    
    let cat1 = new Cat();
    cat1.say();     //"Animal"
    
    //当然,我们还可以继续继承下去
    
    function Tom(){
        this.name = "Tom";
    }
    
    Tom.prototype = new Cat();
    
    Tom.prototype.sayName = function(){
        console.log(this.name);
    }
    
    let cat2 = new Tom();
    cat2.say();     //"Animal"
    cat2.shout();   //"喵喵喵"
    cat2.sayName();     //"Tom"
    cat1.sayName();     //err 报错表示没有该函数
    

    很神奇的,原型链就实现了对象的继承。使用原型链就可以使一个新对象拥有之前对象的所有方法和属性。至于cat1.sayName()会报错,是因为该方法是在它的子原型对象中定义,所以无法找到该函数。但是我相信很多人看到这里还是会一头雾水,到底链在哪里了?谁和谁链在一起了?我用一张图来让大家更好的理解这个。

    原型链

    咋眼一看,这张图信息量不少,但是理解起来却一点都不难。我们先从Animal看起,Animal中存在一个prototype指向其原型对象,这一部分应该没什么问题。但是Animal原型对象中却存在[[prototype]]指向了Object,实际上是指向了Object.prototype这是因为所有函数都是从Object继承而来的所有函数都是Object的实例。这也正是所有的函数都可以拥有Object方法的原因,如toString()。所以这也是原型链的一部分,我们从创建自定义类型开始就已经踏入了原型链中。

    但是这部分我们暂且不管它,我们继续往下面看。我们把Animal的实例当做Cat的原型对象

    Cat.prototype = new Animal();
    

    这样Cat实例就拥有了其父类型的所有方法与属性。因为代码中寻找一个方法会不断往上找,先在实例中寻找,如果没有就在原型对象中去寻找,假如原型对象中没有,就会往原型对象的原型对象中去找,如此递进,最终如果找到则返回,找不到则报错。当我们构成原型链时,会有一个对象原型当做其父类型的实例,这样便形成一条原型链。当然,如果现在有不明白 [[prototype]] (__proto__)与prototype的区别可以去翻看我们第一篇文章,在这就不重复了。

    这样一来我们便明白了为何cat1中没有sayName函数并了解原型链如何实现继承了。但是我又提出了一个问题,假如我们把给子类型原型对象定义方法的位置调换一下,那么会发生什么事呢?

    function Animal(){
        this.type = "Animal";
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        this.vioce = "喵喵喵";
    }
    
    Cat.prototype.shout = function(){
        console.log(this.vioce);
    }
    
    Cat.prototype = new Animal();
    
    let cat1 = new Cat();
    cat1.say();     //"Animal"
    cat1.shuot();       //err,报错无此函数
    

    控制台中会毫不留情的告诉你,没有该方法Uncaught TypeError: cat1.shuot is not a function。这是因为当你把父类的实例赋给子类原型对象时,会将其替换。那么你之前所定义的方法就会失效。所以在这里要注意的一点就是:给原型添加方法时一定要在替换原型语句之后,而且还有一点要注意就是,在用原型链实现继承的时候,千万不可以用字面量形式定义原型方法。不然原型链会断开。

    function Animal(){
        this.type = "Animal";
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        this.vioce = "喵喵喵";
    }
    
    Cat.prototype = new Animal();
    
    Cat.prototype = {       //这样会使上一条语句失效,从而使原型链断开。
        shout:function(){
            console.log(this.vioce);
        }
    }
    

    2.原型链的问题

    接下来我们谈谈原型链的问题。说起原型链的问题我们大概可以联想到原型对象的问题:其属性与方法会被所有实例共享,那么在原型链中亦是如此。

    function Animal(){
        this.type = "Animal";
        this.color = ["white","black","yellow"];
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        this.vioce = "喵喵喵";
    }
    
    Cat.prototype = new Animal();
    
    Cat.prototype.shout = function(){
        console.log(this.vioce);
    }
    
    let cat1 = new Cat();
    let cat2 = new Cat();
    cat1.say();     //"Animal"
    cat1.say();     //"Animal"
    cat1.color.push("pink");
    
    console.log(cat1.color);    //["white", "black", "yellow", "pink"]
    console.log(cat2.color);    //["white", "black", "yellow", "pink"]
    

    当然,这也好理解不是。倘若孙子教会了爷爷某件事,那么爷爷会把他的本领传个他的每个儿子孙子,没毛病对吧。但是我们想要的是,孙子自己学会某件事,但不想让其他人学会。这样意思就是每个实例拥有各自的属性,不与其他实例共享。那么我们就引入了借用构造函数的概念了。

    3.借用构造函数

    借用构造函数,简单来说就是在子类构造函数里面调用父类的构造函数。要怎么调用?可以使用到apply()call()这些方法来实现这个功能。

    function Animal(type = "Animal"){       //设置一个参数,如果子类不传入参数则默认为"Animal"
        this.type = type;
        this.color = ["white","black","yellow"];
    }
    
    function Cat(type){
        Animal.call(this,type);     //继承Animal同时传入type,也可以不传参
    }
    
    let cat1 = new Cat();           //没有传参,type默认为"Animal"
    let cat2 = new Cat("Cat");      //传入"Cat",type则为"Cat"
    
    cat1.color.push("pink");
    
    
    console.log(cat1.color);    //["white", "black", "yellow", "pink"]
    console.log(cat2.color);    //["white", "black", "yellow"]
    console.log(cat1.type);     //"Animal"
    console.log(cat2.type);     //"Cat"
    

    这样就实现了实例属性不共享的功能,而且我们在这个里面还可以传入一个参数,让其向父类传参。这是在原型链里面无法做到的一个功能。至于call()apply()方法,在这暂且不展开,日后另作文章阐明。暂且只需要知道这是改变函数作用域的就行。

    那么,借用构造函数的问题也就是构造函数的问题,方法都定义在构造函数里面了,复用性就基本凉凉。所以,我们要组合起来使用。属性使用借用构造函数模式而方法则使用原型链

    4.组合继承

    function Animal(){
        this.type = "Animal";
        this.color = ["white","black","yellow"];
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        Animal.call(this);      //继承属性
        
        this.vioce = "喵喵喵";
        
    }
    
    Cat.prototype = new Animal();       //继承方法
    
    Cat.prototype.shout = function(){
        console.log(this.vioce);
    }
    
    let cat1 = new Cat();
    let cat2 = new Cat();
    cat1.say();     //"Animal"
    cat1.say();     //"Animal"
    cat1.color.push("pink");
    
    console.log(cat1.color);    //["white", "black", "yellow", "pink"]
    console.log(cat2.color);    //["white", "black", "yellow"]
    
    

    这一套方法也变成了最常用的继承方法了。但是其中也是有个缺陷,就是每次都会调用两次父类的构造函数。从而使得实例中与原型对象中创造相同的属性,不过原型对象中的值却毫无意义。那有没有更完美的方法?有,就是寄生组合式继承。在这里我就放代码给大家。

    function obj(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
    
    function inheritPrototype(sub,super){
        let prototype = obj(super.prototype);       //相当于拷贝了一个父类对象
        prototype.constructor = sub;    增强对象
        sub.prototype = prototype;      指定对象
    }
    function Animal(){
        this.type = "Animal";
        this.color = ["white","black","yellow"];
    }
    
    Animal.prototype.say = function(){
        console.log(this.type);
    }
    
    function Cat(){
        Animal.call(this);      //继承属性
        
        this.vioce = "喵喵喵";
        
    }
    
    inheritPrototype(Cat,Animal);
    
    Cat.prototype.shout = function(){
        console.log(this.vioce);
    }
    
    let cat1 = new Cat();
    let cat2 = new Cat();
    cat1.say();     //"Animal"
    cat1.say();     //"Animal"
    cat1.color.push("pink");
    
    console.log(cat1.color);    //["white", "black", "yellow", "pink"]
    console.log(cat2.color);    //["white", "black", "yellow"]
    

    这样通过一个巧妙的方法就可以少调用一次父类的构造函数,而且不会赋予原型对象中无意义的属性。这是被认为最理想的继承方法。但是最多人用的还是上面那个组合式继承方法。

    总结

    到这原型链的基本概念与用法都已经一一讲述,我们需要注意的地方就是prototype__proto__的关系,重点是分清其中的区别,了解父类型跟其子类型的关系,他们之间的联系在哪。大概要弄懂的地方,就是要把那两文章的两张图吃透,那么我们就已经把原型链吃透大半了。

    最后倘若大家还有什么不懂的地方,或者博主有什么遗漏的地方,欢迎大家指出交流。接下来我还会写一篇关于call()apply()这两个方法的文章。如有兴趣可以持续关注本博主。

    附:《JavaScript由浅及深敲开原型链(一)》

    原创文章,转载请注明出处

    相关文章

      网友评论

      本文标题:JavaScript由浅及深敲开原型链(二)

      本文链接:https://www.haomeiwen.com/subject/dntncftx.html