JavaScript让前端飞Web前端之路

来聊聊JavaScript中"原型链"的个人

2017-10-28  本文已影响47人  长梦未央

此为js入门级文章!
原型链是创建与其他对象相似的对象的一种机制,当你为了节省内存或者避免重复代码,需要两个对象拥有完全相同的属性时,你可能需要从一个对象复制所有的属性到另一个对象。但是JavaScript提供了原型链这种替代方式。通过将第一个对象中的字段查找委派于第二个对象的方式,可以使得一个对象表现的似乎拥有一个对象的所有属性。

下面创建一些有很多相似之处的对象,看看原型链将如何帮助我们。
var gold = {a:1};
console.log(gold.a);

这里我们对对象gold进行属性查找。我们想一下当我们在这个对象中对键a进行属性查找时,每一步将发生什么。解释器开始探究对象gold是否有我们要查找的属性。在查看这个对象的过程中,解释器发现其中有一对以a为键的键值,对应的值为1,因此这个表达式的结果是1。在这种情况下,我们的记录系统将在显示器某粗记录下值1。我们再看一下查找一个此对象并不包含的属性时,会发生什么情况。

console.log(gold.z);

首先开始对此对象进行属性查找,然后解释器对此查找z的尝试答复是undefined。所以在对象gold中查找z的结果为undefined。而这就是最终记录到显示器的结果。

现在我们假设这个程序需要一个与已经创建的对象gold相似的对象。我们可以试着创建一个新的对象,并将所有相同的键值都添加到其中。我们创建一个名为blue的新对象,然后将对象gold中的属性逐一复制到其中,使它跟gold完全一致。假设有个辅助函数可以帮助我们将一个对象中的所有属性都复制到另一个对象中。

var blue = extend({},gold);

虽然这个对于对象blue的复制操作将一直有效,但需要注意这个复制行为只发生在程序执行中的某一个时间点,这并不是一个反复不断的行为,也就是说只有当程序执行到这一步的时候,才会复制过去,但你在这一语句后面添加的其他属性或方法,都没有复制过去,所以这是无法保持两个对象一直同步的。现在我们的gold只有一个属性,因此复制的过程也较为简单,现在这个复制的过程已经完成并且不会再重复。如果我们的程序在稍后修改了gold或blue,那么这两个对象中就只有a这一个相同的属性了。
此时我们向对象blue中添加一个新的属性

blue.b = 2;
console.log(blue.a);
console.log(blue.b);

现在当我们在对象blue中查找被复制过来的属性时,我们可以得到期望的值,即blue.a=1;
对于我们用代码手动添加的属性而言也是一样,blue.b=2。与对象gold相同,如果在blue中查找一个不存在的属性结果应该是undefined。
我们再来将对象gold复制一次,创建一个对象rose。这次我们采用一个不同的方式。我们不用逐一复制属性的方法,而是在对象gold和新对象rose之间建立一种联系,使得在rose中无法查找到某属性时,可以退而求其次在gold中查找。函数Object.creat()可以帮我们创建一个拥有这种委托查找功能的对象,传入你想回退的对象,就可以创建一个新的对象,在字段查找时退回这个对象中查找。你与这个新的特殊对象交互的方式仍然与其它普通对象相同。

var rose = Object.create(gold);
rose.b=2;
console.log(rose.b);

但是当你无法在其中直接查找到某个属性时,就会查找在原型链上端的原型对象。因此console.log(rose.b);也会输出1。
这里注意rose和gold之间显著的相同之处是在查找时实现的,而非之前某个复制过程的结果。对于在原型链下端的对象中能够直接查找到的属性,原型链就不会派上用场。比如rose.b属性的查找,原型链关系在这里并不起作用。对于这个对象中完全不存在的属性,依旧是查找失败。比如console.log(rose.z);会输出undefined。

我们研究一下这两种方式的主要区别

不同之处在于你希望哪一个时刻gold当中的值对另外两个对象产生影响。仅仅是复制的时刻还是每次查找的时刻?修改对象gold的属性就可以看出差别。我们向对象gold中添加新的属性z。

gold.z=3;
console.log(blue.z);

当我们尝试通过在blue中查找属性z的方式来记录新的值3时,会输出undefined。因为blue中没有属性z,并且blue与gold之间并不存在委托关系,因此无处查找。复制操作是一次性的因此gold和blue之间的关系立刻就结束了。这个复制行为保留的成果也只有之前复制的属性a。但是当我们查找属性rose.z时,就会输出3;在对象rose中查找字段属性z,如果找不到就会在原型对象中进行查找。
现在我们只考虑rose和gold之间的关系


如果这种行为可以帮助我们创建一个与对象gold相似的对象rose,那么在JavaScript中是否有其他对象也可以利用这种机制?比如gold是否也会将它的字段查找委托到其他某处?事实也的确如此。有一个最高层级的对象存在,每一个JavaScript对象最终都将失败的查找委托到此处,它为所有的对象提供所有的基本方法。我们将它称为对象原型。因为它提供了整个系统中所有对象的共享属性。正因如此,当我们访问某个对象的 .toString 属性时才可以访问一个实现此功能的函数,当你对此函数有访问权限时,你可以马上调用它。而你进行属性查找的对象将会再函数调用时出现在点符号的左边

rose.toString();

尽管方法rose.toString() 是被存储在原型链的顶端,但是由于参数"this"的工作方式,是的关键词"this"绑定为对象rose。这个共享函数也就可以实现预期效果了。
还有很多其他的辅助方法也非常有用,查看关于它们的文档会有很大帮助。其中最有用的一个属性是 .constructor 可用于确认创建某个对象时使用了哪个参数。

如同所有属性, .constructor 事实上指向存储在别处的另一个对象。大家常常混淆对象原型和这个用于创建所有对象的构造函数。我们暂时想象它们位于两个不同的盒子中,以表示它们在电脑中的不同位置。

当你再一个对象中查找属性 .constructor 时,很大可能这个对象本身并不包含属性 .constructor ,因此将继续在原型链中查找。原型对象中的属性 .constructor 指向创建对象的构造函数。因此结果就是这个对象。
如果没有进行什么特殊的操作,你创建的大多数新对象都会委托于原型对象。但是在JavaScript中创建的某些特殊对象
拥有除所有对象的基本特性之外的特性。比如数组拥有像 .indexOf 和 .slice 这样的方法


这些数组方法被存储于另一个原型之中,即数组原型

由于数组与对象有所不同,甚至数组原型中的某些标准方法,比如 toString 有自己的实现方式,数组原型的上端为对象原型,这样数组中非特有的部分仍可以从对象原型中继承。并非所有的方法都需要在数组原型中重新实现。注意数组原型和对象原型中都有 .constructor属性 尽管其中一个委托于另一个。那么当你在一个数组中查询属性 .constructor 时会发生什么呢?答案是数组中的 constructor .这是因为数组原型中已经拥有属性 .constructor 因此无需再向原型链的上端去委托查询。
这就是JavaScript的原型链,这是一种实现共享代码和节约内存的有效方式。

作者:长梦未央
图片来源:优达学城付费课程
个别文字摘自教学视频老师的讲解

上一篇下一篇

猜你喜欢

热点阅读