简单理解JS原型和原型链

一 前言

        作为一个面向对象的语言,理解语言中的对象概念是十分重要的,而要讨论js中的对象,就离不开js的原型这篇文章会先从理解对象、构造函数开始,进而帮助大家理解原型和原型链,希望可以帮助到正在学习js的小伙伴。(PS:博主也是在学习中发布的这篇博客,不足之处欢迎指正)

二 什么是对象

        对象是什么呢?简单的来说——对象是一种数据结构,他拥有属性和方法。
       
 他往往以这样的形式存在

{
    name:"老赵",
    age:10
    getAge:function(){
        console.log(name)
    }
}

        上述代码中的对象有两个属性 name、age ,有一个方法,getAge(),那么我们如何创建一个对象呢,不同的语言有不同的方法,我们熟知的JAVA是通过实例化类的形式来获得对象的,而js是通过原型,和构造函数来创建对象的。

        这里我们要先有这样的一个概念,原型和构造函数用于创建对象

三 什么是构造函数

        那~什么是构造函数呢?我们可以将构造函数理解为一个生产对象的工厂,构造的意思就是构造对象,其实就是对象的模板在JAVA中我们通过实例化类来创建对象,那么在JS中,我们就是通过实例化构造函数来创建对象

我们可以举一个栗子。

//这里创建了一个构造函数
function Dog (name,age){
        this. name=name;
        this.age=age;
    }
//我们通过new关键字创建一个对象
let dog = new Dog("老赵",2);
//输出这个对象的结构
document.write(JSON.stringify(dog))
//构造出的对象
{"name":"老赵","age":2}

在上述的代码中,我们创建了一个构造函数,然后使用new关键字调用这个构造函数,创建了一个对象,通过对比我们可以很清楚的看到构造函数和对象结构的对应关系。那么构造函数创建对象的详细流程是什么呢?

(1)在内存中创建一个新对象。

(2)构造函数内部的this被赋值为这个新对象(即this指向新对象)。

(3)执行构造函数内部的代码(给新对象添加属性)。

(4)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

结合列出的创建对象的步骤,我们再去看上面的代码,是不是又更清晰了呢。

那么构造函数是不是特殊的呢?JS该如何判断一个函数是不是构造函数呢。其实构造函数不是特殊的,任意一个函数,我们都可以把他当作构造函数来用,用它来创建一个对象。至于构造出的对象到底有什么属性,我们根据上面列出的对象创建过程就可以分析出来了。

比如我们这里随意使用一个函数来创建一个对象。

    function Show(){
        console.log("hello")
    }
    let show = new Show();
    document.write(JSON.stringify(show));

这个函数的功能很简单,就是打印一个hello,我们按照上面的创建规则来分析

第一步 先创建一个新对象,现在我们有一个新对象了

第二步 将this关键字的的指向改为这个新对象,我们没有this关键字,所以忽略

第三步 执行构造函数内部的代码,这里只会打印一个hello

第四步 如果构造函数有非空对象,就返回,否则使用之前创建的对象,我们并没有,所以最终的创建结果就是

{}

一个没有任何属性的对象。

如果大家还有兴趣可以拿别的函数来实验,然后使用上面的流程来分析结果,建议大家实验一下函数直接返回,很有意思的 (😉),这里给大家几个例子。

     function Test1(){
        this.name="老牛"
        return {name :"老赵"}
    }
    function Test2(){
        this.name="老牛"
        return "老赵"
    }

    let test1 = new Test1();
    let test2 = new Test2();
    document.write(JSON.stringify(test1));
    document.write(JSON.stringify(test2));
    //大家可以尝试着自己分析一下结果的原因,很好玩的

四 原型和原型的作用

        看到这里可能就会有小伙伴疑惑了,我们之前不是说“原型和构造函数用于创建对象”,可上面创建对象的步骤中并没有提及到原型啊?,其实没有提及到原型是为了方便大家理解,我把有关原型的步骤省略了,那么接下来就请出我们今天讨论的主角——“原型

 4.1 什么是原型

        我们用最简单的方式理解原型,原型其实就是一个对象,里面存有一些信息。

        那么原型和函数的关系是什么呢?其实原型和函数的关系就像双胞胎

        每个函数都有一个prototype属性,这个属性指向一个对象,也就是原型

        

        我们可以使用上述的图示来描述两者之间的关系,一个函数被创建的时候,这个这个函数对应的原型也会被创建,同时这个函数会有一个属性prototype指向自己的原型,而这个函数的原型,如上图Dog的原型——Dog.prototype中也会有一个属性constructor指向这个函数。两者就像双胞胎一样,每一个函数必定对应一个原型,每一个原型也都有对应的函数,我们在创建一个函数的时候,默认就会创建这个函数对应的原型

4.2 原型的作用

        那么原型有什么作用呢?官方的回答是这样的:“原型中会储存同一构造函数的实例所共享的属性和方法”,听起来可能很抽象,让我们来探寻一下他的原理吧。

        其实上面提出的创建对象的流程被我省略了一步,实际的创建过程是这样的。

(1)在内存中创建一个新对象。

(2)这个对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。

(3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。

(4)执行构造函数内部的代码(给新对象添加属性)。

(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

        我们省略了第二步,那么接下来就让我们来分析一下第二步的内容吧。这个对象(指的就是我们在内存中划出的新对象)内部的[[Prototye]]特性(所谓特性就是不暴露给开发者的属性,但是其实这个属性在某些实现中已经被暴露了,也就是__proto__ 属性)赋值为构造函数的prototype属性。(也就是指对象中的__proto__属性指向构造函数的原型)

我们用下面这张图来描述这个关系

上图中,Dog()这个构造函数的实例 dog 有一个__proto__属性指向原型。也就是说,每一个对象,都有一个__proto__属性,指向自己构造函数的原型。这其实也是一个很有趣的现象,构造函数的对象其实和对象之间没有直接关联,对象直接能访问到的是构造函数的原型

因为对象可以直接访问到原型,所以对象也就可以访问到原型中的属性。因为同一构造器的所有实例都指向同一个原型,所以放在原型中的属性可以被所有的实例公用。

    function Person(){
    }
    Person.prototype.comment="test";

    let person1 = new Person();
    let person2 = new Person();

    document.write(JSON.stringify(person1)+" ");
    document.write(JSON.stringify(person2)+" ");
    document.write(person1.comment+" ");
    document.write(person1.comment===person2.comment);
{} {} test true

        我们可以看到,虽然person1 和 person2 中都没有comment这个属性,但是两者都可以访问到这个属性,同时两者的comment属性完全相同。实际上当我们在person1中去寻找comment属性的时候,当person1中不存在这个属性的时候,js就会去寻找person1所指向的原型,在原型中去寻找。所以我们可以在原型中储存一些公共元素,可以让相同构造器下的所有实例都访问到这些属性。

         原型中属性的公共性其实主要体现在引用数据类型上,如果我们通过构造函数在实例中创造出一个方法


    function Person(){
        this. show = function(){}
    }

    let person1 = new Person();
    let person2 = new Person();
    document.write(person1.show==person2.show);
false

        我们可以看到,不同实例的相同方法,即使名称相同,但是两者并不相同,因为两者存在与不同的内存空间中,相当于我们把同一个函数创造了两遍,这样无疑会浪费空间,所以我们一般把这些公共的方法放在protortyoe中。

     function Person(){
    }
    Person.prototype.show = function(){}
    let person1 = new Person();
    let person2 = new Person();
    document.write(person1.show==person2.show);
true

        这样就可以创造出一个真正的单例公共函数。

       

4.3 实例修改原型属性

        实例是无法修改原型中的属性的,实例对原型的修改会在实例中创造出一个同名的属性,并不会修改原型中的属性。

     function Person(){
    }
    //我们在原型中创造一个公用属性
    Person.prototype.comment="test";

    let person1 = new Person();
    let person2 = new Person();

    //试图在person1中修改共有属性
    person1.comment="mytest";
    
    //表面上看,person1好像修改了在原型中的属性
    document.write(person1.comment+" ") //mytest
    //其实只是在person1中创造了一个同名的属性,所以调用后的结果为mytest
    document.write(JSON.stringify(person1)+" ") //{comment:mytest}
    //我们使用person2调用prototype中的属性,发现并没有被修改
    document.write(person2.comment+" ") //test

         但是实例是可以修改在prototype中对象的属性的,因为修改对象的属性并没有修改prototype属性的指向,所以不会被认为是修改了propertype的属性。

    function Person(){
    }
    //我们在原型中创造一个公用对象
    Person.prototype.obj={};

    let person1 = new Person();
    let person2 = new Person();

    //给公共对象添加属性
    person1.obj.name="老赵";
    
    //我们可以用person2查看到修改
    document.write(JSON.stringify(person2.obj)) //{"name":"老赵"}
    //person1 中也没有新建同名属性
    document.write(JSON.stringify(person1))//{}

        同时这样是原型的缺点,强大的共享能力,使得原型中不适合放入对象。

4.4 总结

        我们上面主要讲明了对象,原型,构造函数之间的关系——构造函数和原型用于创建函数构造函数中的语句会作为对象的模板,“指导”对象在内存中创建相应的属性,而原型中储存了所有实例对象共享的属性,实例对象可以通过指向原型的指针来访问原型中的公共属性

        此外,原型和构造函数是一对双生子每一个构造函数都有对应的原型,两者可以分别通过.prototype 和 .constructor来访问彼此,形象点的解释,构造函数和原型就是对象的爹和妈。两者分别以不同的方式让实例对象(孩子)获得属性。

        一旦一个构造函数被创造,就会形成这样的链条。

        一旦一个对象被实例化出来,就会在链条的末尾添上一个对象。

        好了,基础知识咱们就讲完了,接下来就是咱们的重头戏,原型链了。

五 原型链

        在如果你对前面的知识已经理解的差不多了,那么恭喜你,因为你再去学习原型链将会非常容易,如果你对前面的知识并不是十分理解,那么建议你停下来好好理解一下,再来进行原型链的学习,因为原型链其实就是多个原型拼接起来的链条而已。

原型链(一)

        我们前面提到过,原型是一个对象,那么根据我们之前的理论,每个对象都是由一个构造函数和构造函数原型来进行创造的,那么是谁创造了原型呢,我们用一个图来解释。

        其实就是Object这个这个构造器(没错,Object其实是个函数)和Object.prototype 创造了构造函数的prototype。那么是谁创造了Object.prototype呢。

         对,就是null(😂)这个对象是js预设好的,我们可以理解为他就是原型链的终点,万物的伊始,类似于JAVA中的Object。

        这样我们就形成了一个链条

         对象实例可以沿着链条逐步向前访问各prototype中的内容。

    function Person(){}
    //我们在原型链上各个prototyp中添加内容
    Person.prototype.add1="add1";
    Object.prototype.add2="add2";
    //实例化一个对象
    let person = new Person();
    //可以访问到原型链上的内容
    document.write(person.add1); //add1
    document.write(person.add2); //add2

这样就形成了一条对象的原型链了。

原型链(二)

        我们知道,其实在js中函数也是一种对象,那么既然是对象就一定会有构造函数和对应的原型,那么,构造函数的原型是什么呢。

         其实就是Function,这个其实好理解,我们创建的每一个函数对象,都是Function的实例,所以Dog的.__proto__属性就指向Function.prototype,Functon就是最初的构造函数。

        那么~~,Function这个构造函数的原型是什么呢?(🤣)

        

         没错,就是Function,既然Function这个构造函数他是个函数,那么他的__proto__指向所有函数的原型Function.function也挺合情合理的吧(都是JS底层规定好的)

        接下来我们继续寻找,Function.prototype的原型。既然他是个对象(我们前面说过函数的prototype是一个对象)那么他的原型就应该指向Object.prototype,所以最终我们的原型链就完成了。

  

         是不是有些理解了,其实核心思路就是对象的创建需要构造函数和原型对象指向原型,所有的没有经过显式构造器创建的对象默认通过Object构造创建,指向Object.prototype,所有的函数默认通过Function构造器构造,指向Function.Prototype,原型和构造函数之间可以通过constructor和prototye相互访问(划重点😁)

        我自己画的图并不好,这里给大家提供另一个博主画的图,方便大家理解


图片转载与

六 写在后面

        原型链的内容就给大家讲完了,本篇文章的主要目的是让大家对原型和原型链有一个初步的认识,奠定一定的基础,如果大家想对原型链有更深入的认识和理解,建议去看一下其他的教程,博主自己是通过看JavaScript高级程序设计(第四版)这本书来进行学习的,书内的第八章的主要内容就是对象和原型链,书内还有很多好玩的内容,比如说通过原型链来实现类与继承。鼓励大家多使用原型链的知识去尝试解释一些js的语法,会有更好的理解,比如js的数据类型。

        好了,这篇博客到这里就结束了,欢迎大家在评论区进行讨论。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值