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

一、对象的继承

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由浅及深敲开原型链(一)》

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容