文章目录
一、一个实例初识原型是什么?
原型(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__上去查找,这样一层一层向上查找就形成了链式结构。