简单剖析javascript——原型*原型链

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

对于作用域和作用域链,想必大家都很熟悉了,那算是javascript中的一个小山,虽然小路崎岖,但也还能理解,可是对比原型以及原型链来说,那它还不够资格称为“崎岖”。可尽管如此,我们仍然要去揭开javascript中原型以及原型链那神秘的面纱。

本文涉及的知识点:

· __proto__和prototype

· constructor

· 构造函数

· 作用域链

原型及原型链的定义

原型:

在JS中每个构造器(函数)都有一个属性叫prototype,它叫原型,也是个对象,我们叫这个对象为原型对象;而每个对象中有一个属性叫__proto__,它叫隐式原型

原型链:

原型链是一个对象的查找机制,比如查找对象arr中的toString方法,会先在自己的私有属性中找,如果没有,就沿着__proto__去原型对象中找,如果还没有,就继续沿着__proto__去它原型对象中的原型对象中找,直到找到Object中的原型对象(Object原型对象中的__proto__指向null),如果还没找到,那么结果就是undefined;

原型

什么是原型?

讲到原型,我们通常讲的一般都是显式原型,也就是函数原型。而在通俗的角度上我们可以理解为显式原型是函数对象的“爹”,函数对象是能够继承到显式原型的属性,也可以理解为继承到它“爹”的血脉。

如:

扫描二维码关注公众号,回复: 14374794 查看本文章
Person.prototype.name = '爹'
function Person(){

}
var person = new Person()
console.log(person.name);
复制代码

通常的情况下,我们new的这个函数对象里面是空的,没有任何属性,所以我们这console.log(person.name)理应是undefined.

然而,事实是,打印出来的却是Person.prototype中的name属性“爹”.

1657692881839.png

值得一提的是,浏览器引擎会优先在自身函数里查找是否存在这个属性,如果自身函数体内存在需要查找的属性的话,它就不会再去他的“爹”里面去查找。

附图:

1657693168809.png

当然,除了能找到它“爹”的属性,也是能找到它“爹”的函数啦!

Person.prototype.name = '爹'
Person.prototype.say = function() {
    console.log('这是爹的血脉')
}
function Person(){
    this.name='儿子'
}
var person = new Person()
// console.log(person.name);
person.say()
复制代码
1657693709994.png

而且奥,如果这个构造函数生成了一万个对象,那么它就会复刻出一万份属性出来,但是这个函数对象的原型却只有这一个,不会再复刻。就像一个父亲能有很多个孩子,但是这几个孩子却只能有一个父亲,他们的血脉以及基因都是来源于这个父亲的。

如图:

1657709254145.png

来,一起进一步探讨原型,我们可以先写下以下代码:

function Person(){
}
var person = new Person()
person
复制代码

然后我们可以在控制台看到以下代码:

1657709729837.png

打开person{}

1657709747643.png

在此,我们知道这个对象函数是没有写入任何属性的,但是可以看到person{}底下的目录有[[Prototype]]这个属性(其实我们更习惯称它为__proto__的),它就是对象原型,也被我们称之为隐式原型;这里我们虽然没有给对象函数写入任何属性,但是,这个对象仍然是会继承它的原型里的属性,一直到隐式原型为空停止:

1657711253880.png

可以看到的是,它的隐式原型一层套着一层,类似套娃,层层叠叠,知道最后的__proto__为null停止。

原型的作用

一般情况下,我们写一个构造函数,然后去通过对象去执行函数,每一次调用这个对象里的这些属性,这个构造函数里面的属性都会随之重新构造,多少会对整个代码运行的效率和性能造成一定的影响。

function Car(color,owner){
    this.color = color
    this.owner = owner
    this.carName = '宝马'
    this.carheight = 1400
    this.carlang = 4900
}
var car1 = new Car('red','李')
var car2 = new Car('green','刘')
console.log(car1.carName)
console.log(car2.carName)

复制代码

这里,我们可以把构造函数Car()函数看成是一个宝马造车间,而对象(也就是买主)可以定制车的颜色以及车主的名字,我们可以传参来定制车的颜色以及车主的名字,而车的牌子和长宽却已经是定死的了。但是每当我们调用这个对象时,车间又得重新去做一个车的尺寸,和定制颜色一样,每次又得重新开始做,这样会对造车间的效率造成影响。

1657713522505.png

但是原型不需要,它就像是血脉刻在骨子里的一样,而且原型也是一个对象,我们可以拿原型来承装那些共有不变的属性,既美观又能提高效率。

function Car(color,owner){
    this.color = color
    this.owner = owner
}
Car.prototype = {
    carName:'宝马',
    carheight:1400,
    carlang:4900
}

var car1 = new Car('red','李')
var car2 = new Car('green','刘')
console.log(car1.carName)
console.log(car2.carName)

复制代码

这样子,我们就相当于有一个批量生产这些车零件的流水线,每次只要拿到现成的零件进行拼接就可以了,大大提高了效率。

1657713956094.png

最后的效果还是和之前一样的,没有任何变化。即利用原型的特点和概念,可以实现公有属性。

原型的增删改查

原型的增删改查只能由原型本身去做,去执行,通过函数体对象是无法对其进行增删该查的,即便执行,也只不过是对后面的对象进行的增删改查。

可以看以下代码

Person.prototype.LastName='杨'
function Person(name){
    this.name=name
}

var person1 = new Person('大公子')
person1.LastName='陈'
console.log(person1.LastName)

var person2 = new Person('二公子')
console.log(person2.LastName)
复制代码

这里咱们打个比喻,这里的Person()可以理解为一个“杨”氏家族,var person1 = new Person('大公子')则是杨氏家族的大公子,var person2 = new Person('二公子')是杨氏家族的二公子。

有一天杨大公子突然不想姓杨了,他想改姓随着母亲“陈”氏去姓,于是person1.LastName='陈'改变自己的姓,那么杨二公子的姓呢?

1657715128446.png

很显然,二公子仍然是姓杨,大公子再怎么去改姓,也不能去改变他祖先传下来的姓氏,那么二公子仍然姓杨,如果想要改变整个杨氏家族的姓,那么只能从源头上去改变,那就是他的父亲以及祖先改姓。

即代码将改为

Person.prototype.LastName='杨'
function Person(name){
    this.name=name
}

var person1 = new Person('大公子')
Person.prototype.LastName='陈'
console.log(person1.LastName)

var person2 = new Person('二公子')
console.log(person2.LastName)
复制代码

那么从此往后,就没有杨氏宗族了,就是陈氏家族了。

同样的,原型的增删也是同样的原理,只能从源头去执行这些操作才能彻底改变原型里的属性数据,不然个体再如何去修改也是个体,无法去影响整个函数体。

还有一个结论注意嗷,实例对象的隐式原型等同于构造函数的显式原型(具体大家可以比对上面的代码)

原型链

原型链学习参考图

lQLPJxZ57MApVhfNBe_NBMWwCS12_vQnHK4CyNPyBgAnAQ_1221_1519.png

这张原型链图应该可以说是每一个学习原型链的人都应该看的,都应该去学习的一张图,也是每一个前端工程师应该拎得清的东西。接下来,我会带着大家一起来了解这一张图。

细剖原型链

首先,我们先来看它,构造函数Foo()

1657717916093.png

这里的Foo()有一个prototype的实线箭头指向Foo.prototype,这说明构造函数的显式原型是Foo.prototype;而1它的背后还有一条__proto__虚线箭头指向下方的Function.prototype,这正应了那句话,万物皆对象,而构造函数也是对象,是一个function Function()创建出来的实例对象,它的隐式原型则会指向function Function()函数的显式原型,这又应了另外一句话实例对象的隐式原型等同于构造函数的显式原型

然后,我们再来看它,实例对象f1,f2;

1657718894270.png

它们都是Function Foo()的实例对象,故他们的隐式原型__proto__指向的就是Function Foo()的显式原型,而Function Foo()的显式原型prototype将实线箭头就是指向Foo.prototype的(即Function Foo()的显式原型),实例对象f1,f2的隐式原型__proto__指向也是它。

接下来,我们看到Foo.prototype;

1657719119808.png

这里可以看到,Foo.prototype有一条实线constructor指向的是Function Foo(),这说明原型Foo.prototype是由Function Foo()构造的,constructor是构造器属性,它会将原型指向它的构造函数。

而同样Foo.prototype也可以看作是一个实例对象,它的隐式原型就是它的构造函数的显式原型。对象的构造函数则是function Object()

1657721465003.png

那么function Object()的显式原型Object.prototype就是Foo.prototype的隐式原型了

用样的道理,O1和O2也是如此。

1657721705392.png

它们也是通过function Object()构造的实例对象,(对了,这里相信已经有不少人从刚才的把原型Foo.prototype看做对象开始那里就有些好奇Object()构造函数是从哪里来的吧?其实Object()函数是系统已经封装好的函数,可以理解为是所有无构造函数对象的构造函数,function Function()也是如此)故它的隐式原型同样是指向Object.prototype的。

最后,我们来看像Obect.prototype这里

1657722120242.png

此时,Object.prototype已经没有了它的构造函数,没有了构造函数,那么就没有构造函数的显式原型,同样的,Object.prototype的隐式原型就是null了。

这就是原型链的剖析啦,原型链可以理解为:在原型再加一个原型,再加一个原型......把原型连成链,访问顺序也是依照这个链的顺序跟作用域链一样(作用域链可以参考我的另一篇写作用域以及作用域链的文章),叫做原型链。

结束语

也许原型和原型链刚接触会显得很难,但是当你积累了一些知识之后,再回首,你会发现其实它好像没那么难鸭。不要纠结此时没办法理解,只要你有心,终究会成功的!这句话送给你们,也是给我自己的鼓励。

猜你喜欢

转载自juejin.im/post/7119867515517435918