前端面试必考——原型及原型链(涵实例+话术)


一、一个实例初识原型是什么?

原型(prototype):对于每一个function对象都可以利用x.prototype访问到,既然用到了.语法,那也就意味着它可以被看做是一个属性,打印出来结果如下。

        var a=function (){}
        console.log(a.prototype)

在这里插入图片描述
可以看到它是一个对象,(就是一个普通的对象)其中有一个constructor:f(),这也就意味着它代表这个构造函数本身。代表实例化之前的东西。这里好像还是有点看不明白它究竟是什么,那我们继续操作它试一试:
前文说了它是一个对象,那我们就可以为它再添加一些属性:

  var Car=function(obj)
        {
            this.name=obj.name
            this.price=obj.price
        }
        Car.prototype.size='suv'
        var Bm=new Car({name:'宝马',price:'1000000'})
        var Bc=new Car({name:'奔驰',price:'2000000'})
        console.log(Bc,Bm)
        console.log(Bc.size,Bm.size)

我们为propotype添加了一个size属性,然后我们试着用它实例化出来的对象打印这个属性:
在这里插入图片描述
我们可以看到这两个对象也都拥有了这个size属性,那么这也就说明了一个问题:
这个prototype是定义构造函数构造出的每个对象的公共祖先。
通俗的说,只要对某个function的prototype进行了属性的增加,那么接下来所有利用这个function构造出来的对象都可以继承此属性。那我们再尝试下如下操作:

        var Car=function(obj)
        {
            this.name=obj.name
            this.price=obj.price
            
        }
        Car.prototype.price=2500
        var Bm=new Car({name:'宝马',price:'1000000'})
        var Bc=new Car({name:'奔驰',price:'2000000'})
        console.log(Bc,Bm)

在这里插入图片描述
可以看到price并未修改,这也就是说prototype的优先级小于自身(直接为构造函数增添this.xx=xx/为实例化的对象直接增添属性)。
总结:当要调用某对象的属性或方法先去查找自身有无,若没有再去查找prototype中是否有相应属性或方法。

二、应用场景

既然我们的prototype和利用this本身设置出来的效果没有什么差距,那我们为什么还要利用prototype呢?思考这么一个问题:当我们实例化某些对象时,若他们有一个公共属性或方法,如果每次实例化都用this赋值,就会造成冗余,每次都要初始化这个属性或方法,那么此时我们就可以将这个属性或方法写到prototype中去,这样每个实例化的对象就默认含有该属性或方法,提高了效率,减少代码冗余。
实际场景中,通常将方法写到原型中去,而属性利用this。

三、升级设置

如果我们想要通过原型对它设置多个prototype,那我们应该怎么写呢?

		Car.prototype.size=2500
        Car.prototype.weight=8000
        Car.prototype.color='white'

这样是可以成功设置,但是代码明显有些冗余,因为前文我们说过它是一个对象所以我们可以:

 Car.prototype={
            size:2500,
            weight:8000,
            color:'white',
            speed:function(){console.log('speed')}
        }

但要注意,此时相当于重写了这个方法,而文章开篇我们知道prototype自身包含一个constructor属性,这样重写后它不再包含此属性。

四、__proto__与prototype

我们首先明确一下prototype是什么时候产生的呢,不难发现这都是实例化后prototype才会产生,我们知道var a=new xxx()的过程其实是在预编译过程为 new对应得function()添加了this={},执行时为this这个对象赋值,然后返回this;那么我们打印一下上文中实例化的Bc这个对象,在这里插入图片描述
可以看到里面有了name、price这两个属性,其实意味着JS引擎偷偷的执行了为this这个对象赋值的步骤,但我们又看到里面新添了一个__proto__这个属性,那么也就说明这个__proto__属于这个对象实例,也就是这么一个过程:

var this={__proto__:xxx}

那么继续实验,我们利用prototype为该function添加一个color属性:

       Car.prototype.color='white'

在这里插入图片描述
可以看到这个color加到了__proro__中,再追其根本其实也就是


var this={__proto__:{color:'white'}}

也就意味着


var this={__proto__:Car.prototype}

所以说我们的__proto__与prototype的关系是键与值之间的关系、
总结一下全过程:对于一个对象xx来说当利用它的构造函数XX.prototype去声明方法属性时,对于对象xx来说它其实并没有真的存在,而是等待着实例,一旦实例化了就真的存在,并添加到该实例化对象的this{}中的__proto__中去。所以__proto__并没有什么深奥的,就是一个装prototype的容器,当想调用对象的某方法属性时,先找自己,自己没有就去__proto__中找。

深入理解constructor与__proto__

我们来看一个例子,用这个例子来了解全过程:

        Car.prototype.name='Bob'
        function Car(){}
        var car=new Car()
        Car.prototype={name:'Alice'}
        console.log(car) //Bob
        var car2=new Car()
        console.log(car2)//Alice

神奇的来了,先分析car这个对象:
在这里插入图片描述

可以看到该实例化对象car本身的__proto__有一个name属性为Bob,而点开constructor(我们知道它指向函数本身所以会有函数相关的属性:length、arguments、prototype等),其中的prototype只有一个name,注意!是只有,但我们前文说prototype是包含constructor的但这一次没有、这就奇怪了。但其实原理也很简单我们的代码意图如下:在实例化前为prototype增添属性,实例化后重写构造函数的prototype(注意是重写的构造函数的prototype!而重写就将constructor写没了…),所以我们当前的car去找它的proto还是找的重写前,毕竟重写发生在它诞生后嘛,所我们的实例化对象若打印car.name为Bob。
再来看一下我们的car2:
在这里插入图片描述
可以看到此时car2的proto上并无constructor属性了,因为我们的car2诞生于重写构造函数prototype之后,所以实例化它的时候这个new过程中的this={ proto:prototype}当然是将重写后的prototype赋值给prototype啦,自然就没有constructor,只有一个name:Alice。


按照时间顺序再说一下这个过程:在Car这个函数定义时它就会有一个prototype属性,该属性下有一个constructor属性(constructor指向该函数本身),所以这个constructor属性内就还是包含这个函数本身初始化的prototype…你可以无限点下去因为它就是指向自己的。当利用这个函数实例化对象时,这个实例化出来的对象生成包含构造函数的prototype的容器__proto__,既然它包含prototype所以__proto__中就也有constructor,那么就从这个constructor的prototype中取出属性、方法,使它成为自己的,所以在实例化之后重写这个prototype是对constructor中的prototype重写,对该对象本身无影响。

总结一下: 实例化就是从这个构造函数的constructor中,找出prototype并赋值给__proto__的过程。

所以在面试过程中遇到了这么一个问题我们应该怎么回答呢?

原型:所有的引用类型都有一个__proto__(隐式原型)属性,它是一个普通的对象、
所有的函数都有一个prototype属性(显式原型),是一个普通的对象。
所有引用类型的__proto__指向它的构造函数的prototype属性。

五、原型链(建立在原型继承的基础上去理解)

相信通过刚才的介绍,你已经明白了什么是原型,那么我们来说一下原型链:
原型链就是沿着原型一层一层去向上找,形成的这个链条其实就是原型链;也就是说当你调用某个对象的属性或方法,它自身没有,就去找他继承的prototype若还没有就在找它prototype所继承的prototype层层向上。

        Professor.prototype.tSkill='JAVA'
        function Professor(){}
        var professor=new Professor()
        Teacher.prototype=professor
        function Teacher(){
            this.mSkill='JS/JQ'
        }
        var teacher=new Teacher()
       
        Student.prototype=teacher
        function  Student ()
        {
            this.pSkill='HTML/CSS'
        }
        var student=new Student()
        console.log(student)

在这里插入图片描述
这就是一个经典的原型继承:student继承teacher继承professor,原型本身还有原型——>那终点是什么呢?是Object.prototype。
我们再来思考,是否可以通过子代去修改父类的属性呢?

  		Professor.prototype.tSkill='JAVA'
        function Professor(){}
        var professor=new Professor()
        
        Teacher.prototype=professor
        function Teacher(){
            this.mSkill='JS/JQ',
            this.success={math:100,english:100}
        }
        var teacher=new Teacher()
       
        Student.prototype=teacher
        function  Student ()
        {
            this.pSkill='HTML/CSS'
        }
        var student=new Student()
        student.success.math=50
        console.log(teacher)



代码含义:Student这个function的prototype已经被teacher赋值。(也就是原型继承)然后通过student修改Teacher的success,对于teacher来说会有变化吗?

在这里插入图片描述
可以看到teacher也发生了变化,

那我们如果是通过子类修改父类的mskill属性呢:

        student.mSkill='java'

在这里插入图片描述

父类的并没有修改。
所以,其实我们也可以通过子类修改父类的某些属性,且该属性必须为引用值,原始值并不可以,因为子类和父类当修改子类的引用类型的某些方法,其实修改的是父类的(此时因为父类也有此方法,所以子类该方法指向父类,也就是同一个。

总结:对子类的原始值属性修改时父类无影响,对子类引用值修改时,若父类也有该属性,则父类也改变

面试中如何回答呢?
原型链:当访问一个对象的属性时,先去这个对象的本身属性上去查找,若找不到就去他的__proto__上(即它的构造函数的prototype)去查找,若还是找不到就去它的构造函数的prototype的__proto__上去查找,这样一层一层向上查找就形成了链式结构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值