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

1、对象的继承

1.了解原型链

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

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()。因此这也是原型链的一部分,咱们从建立自定义类型开始就已经踏入了原型链中。this

可是这部分咱们暂且无论它,咱们继续往下面看。咱们把Animal的实例当作Cat的原型对象spa

Cat.prototype = new Animal();

这样Cat实例就拥有了其父类型的全部方法与属性。由于代码中寻找一个方法会不断往上找,先在实例中寻找,若是没有就在原型对象中去寻找,假如原型对象中没有,就会往原型对象的原型对象中去找,如此递进,最终若是找到则返回,找不到则报错。当咱们构成原型链时,会有一个对象原型当作其父类型的实例,这样便造成一条原型链。固然,若是如今有不明白 [[prototype]] (__proto__)与prototype的区别能够去翻看咱们第一篇文章,在这就不重复了。prototype

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

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__的关系,重点是分清其中的区别,了解父类型跟其子类型的关系,他们之间的联系在哪。大概要弄懂的地方,就是要把那两文章的两张图吃透,那么咱们就已经把原型链吃透大半了。

最后假若你们还有什么不懂的地方,或者博主有什么遗漏的地方,欢迎你们指出交流。若有兴趣能够持续关注本博主。

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

相关文章
相关标签/搜索