资讯专栏INFORMATION COLUMN

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

Carbs / 1508人阅读

摘要:不然原型链会断开。喵喵喵这样会使上一条语句失效,从而使原型链断开。这是在原型链里面无法做到的一个功能。属性使用借用构造函数模式,而方法则使用原型链。

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

最后倘若大家还有什么不懂的地方,或者博主有什么遗漏的地方,欢迎大家指出交流。如有兴趣可以持续关注本博主。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94649.html

相关文章

  • JavaScript由浅及深了解原型链(一)

    摘要:但是该方法有个缺点就是看不出该对象的类型,于是乎构造函数模式应运而生。当然,如果细心的朋友应该会发现函数名首字母大写了,这是约定在构造函数时将首字母大写。这时候,聪明的人应该都可以想到,将构造函数模式和原型模式组合起来就可以了。 一.什么是js对象 1.简单理解js对象 在了解原型链之前,我们先要弄清楚什么是JavaScript的对象,JavaScript对象又由哪些组成。有人说一个程...

    YorkChen 评论0 收藏0
  • SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程

    摘要:函数式编程,一看这个词,简直就是学院派的典范。所以这期周刊,我们就重点引入的函数式编程,浅入浅出,一窥函数式编程的思想,可能让你对编程语言的理解更加融会贯通一些。但从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。 showImg(https://segmentfault.com/img/bVGQuc); 函数式编程(Functional Programming),一...

    csRyan 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • 区块链概念 That You Must Know 第四期(2)

    摘要:下图给出一个简单的列表图什么是哈希和哈希值为理解挖矿的代码机制,首先解决几个概念。第一个就是哈希。哈希值为十六进制表示的数,且长度固定。也正是哈希值的这些特点,赋予了其加密信息时更高的安全性。 第四期 挖矿的相关算法(2) 卡酷少Wechat:13260325501 看过(1)篇,相信你一定对挖矿的机制有了一点了解。那么本篇,我们来一起看一下挖矿中涉及的算法。 在本篇文章中,如果在...

    Sourcelink 评论0 收藏0
  • 移除注释的完善思路:真的可以用正则实现?

    摘要:导语网上有很多自称能实现移除注释的正则表达式,实际上存在种种缺陷。为了避免正则的记忆功能,都使用了正则字面量进行测试。下面是去除单行注释的最终代码。修改方式将删除注释的正则改为。无法正确的移除引号块。 导语 网上有很多自称能实现移除JS注释的正则表达式,实际上存在种种缺陷。这使人多少有些愕然,也不禁疑惑到:真的可以用正则实现吗?而本篇文章以使用正则移除JS注释为目标,通过实践,由浅及深...

    WalkerXu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<