前言前端
你所不懂js连载中断几天以后,今天它又来了。相信这又是让大家一篇稍后阅读的文章了。今天继续由前端早读课专栏做者@HetfieldJoe带来连载《你不懂JS》的分享。ps:基础原理老是苦涩的。算法
正文从这开始~编程
你不懂JS:this与对象原型 第五章:原型(Prototype)设计模式
在【第767期】你不懂JS:混合(淆)“类”的对象 【第766期】你不懂JS:对象中,咱们几回提到了[[Prototype]]链,但咱们没有讨论它究竟是什么。如今咱们就详细讲解一下原型(prototype)。浏览器
注意: 全部模拟类拷贝行为的企图,也就是咱们在前面第四章描述的内容,称为各类种类的“mixin”,和咱们要在本章中讲解的[[Prototype]]链机制彻底不一样。安全
[[Prototype]]框架
JavaScript中的对象有一个内部属性,在语言规范中称为[[Prototype]],它只是一个其余对象的引用。几乎全部的对象在被建立时,它的这个属性都被赋予了一个非null值。ide
注意: 咱们立刻就会看到,一个对象拥有一个空的[[Prototype]]连接是 可能 的,虽然这有些不寻常。函数
考虑下面的代码:工具
[[Prototype]]引用有什么用?在【第766期】你不懂JS:对象中,咱们讲解了[[Get]]操做,它会在你引用一个对象上的属性时被调用,好比myObject.a。对于默认的[[Get]]操做来讲,第一步就是检查对象自己是否拥有一个a属性,若是有,就使用它。
注意: ES6的代理(Proxy)超出了咱们要在本书内讨论的范围(将会在本系列的后续书目中涵盖!),可是若是加入Proxy,咱们在这里讨论的关于普通[[Get]]和[[Put]]的行为都是不被采用的。
可是若是myObject上 不 存在a属性时,咱们就将注意力转向对象的[[Prototype]]链。
若是默认的[[Get]]操做不能直接在对象上找到被请求的属性,那么会沿着对象的[[Prototype]]链 继续处理。
注意: 咱们立刻就会解释Object.create(..)是作什么,如何作的。眼下先假设,它建立了一个对象,这个对象带有一个链到指定的对象的[[Prototype]]连接,这个连接就是咱们要讲解的。
那么,咱们如今让myObject``[[Prototype]]链到了anotherObject。虽然很明显myObject.a实际上不存在,可是不管如何属性访问成功了(在anotherObject中找到了),并且确实找到了值2。
可是,若是在anotherObject上也没有找到a,并且若是它的[[Prototype]]链不为空,就沿着它继续查找。
这个处理持续进行,直到找到名称匹配的属性,或者[[Prototype]]链终结。若是在链条的末尾都没有找到匹配的属性,那么[[Get]]操做的返回结果为undefined。
和这种[[Prototype]]链查询处理类似,若是你使用for..in循环迭代一个对象,全部在它的链条上能够到达的(而且是enumerable——见第三章)属性都会被枚举。若是你使用in操做符来测试一个属性在一个对象上的存在性,in将会检查对象的整个链条(无论 可枚举性)。
因此,当你以各类方式进行属性查询时,[[Prototype]]链就会一个连接一个连接地被查询。一旦找到属性或者链条终结,这种查询会就会中止。
Object.prototype
可是[[Prototype]]链到底在 哪里 “终结”?
每一个 普通 的[[Prototype]]链的最顶端,是内建的Object.prototype。这个对象包含各类在整个JS中被使用的共通工具,由于JavaScript中全部普通(内建,而非被宿主环境扩展的)的对象都“衍生自”(也就是,使它们的[[Prototype]]顶端为)Object.prototype对象。
你会在这里发现一些你可能很熟悉的工具,好比.toString()和.valueOf()。在第三章中,咱们介绍了另外一个:.hasOwnProperty(..)。还有另一个你可能不太熟悉,但咱们将在这一章里讨论的Object.prototype上的函数是.isPrototypeOf(..)。
设置与遮蔽属性
回到第三章,咱们提到过在对象上设置属性要比仅仅在对象上添加新属性或改变既存属性的值更加微妙。如今咱们将更完整地重温这个话题。
若是myObject对象已直接经拥有了普通的名为foo的数据访问器属性,那么这个赋值就和改变既存属性的值同样简单。
若是foo尚未直接存在于myObject,[[Prototype]]就会被遍历,就像[[Get]]操做那样。若是在链条的任何地方都没有找到foo,那么就会像咱们指望的那样,属性foo就以指定的值被直接添加到myObject上。
然而,若是foo已经存在于链条更高层的某处,myObject.foo = "bar"赋值就可能会发生微妙的(也许使人诧异的)行为。咱们一下子就详细讲解。
若是属性名foo同时存在于myObject自己和从myObject开始的[[Prototype]]链的更高层,这样的状况称为 遮蔽。直接存在于myObject上的foo属性会 遮蔽 任何出如今链条高层的foo属性,由于myObject.foo查询老是在寻找链条最底层的foo属性。
正如咱们被暗示的那样,在myObject上的foo遮蔽没有看起来那么简单。咱们如今来考察myObject.foo = "bar"赋值的三种场景,当foo 不直接存在 于myObject,但 存在 于myObject的[[Prototype]]链的更高层:
若是一个普通的名为foo的数据访问属性在[[Prototype]]链的高层某处被找到,并且没有被标记为只读(writable:false),那么一个名为foo的新属性就直接添加到myObject上,造成一个 遮蔽属性。
若是一个foo在[[Prototype]]链的高层某处被找到,可是它被标记为 只读(writable:false) ,那么设置既存属性和在myObject上建立遮蔽属性都是 不容许 的。若是代码运行在strict mode下,一个错误会被抛出。不然,这个设置属性值的操做会被无声地忽略。不论怎样,没有发生遮蔽。
若是一个foo在[[Prototype]]链的高层某处被找到,并且它是一个setter(见第三章),那么这个setter老是被调用。没有foo会被添加到(也就是遮蔽在)myObject上,这个foosetter也不会被重定义。
大多数开发者认为,若是一个属性已经存在于[[Prototype]]链的高层,那么对它的赋值([[Put]])将老是形成遮蔽。但如你所见,这仅在刚才描述的三中场景中的一种(第一种)中是对的。
若是你想在第二和第三种状况中遮蔽foo,那你就不能使用=赋值,而必须使用Object.defineProperty(..)(见第三章)将foo添加到myObject。
注意: 第二种状况多是三种状况中最让人诧异的了。只读 属性的存在会阻止同名属性在[[Prototype]]链的低层被建立(遮蔽)。这个限制的主要缘由是为了加强类继承属性的幻觉。若是你想象位于链条高层的foo被继承(拷贝)至myObject, 那么在myObject上强制foo属性不可写就有道理。但若是你将幻觉和现实分开,并且认识到 实际上 没有这样的继承拷贝发生(见第四,五章),那么仅由于某些其余的对象上拥有不可写的foo,而致使myObject不能拥有foo属性就有些不天然。并且更奇怪的是,这个限制仅限于=赋值,当使用Object.defineProperty(..)时不被强制。
若是你须要在方法间进行委托,方法 的遮蔽会致使难看的 显式假想多态(见第四章)。通常来讲,遮蔽与它带来的好处相比太过复杂和微妙了,因此你应当尽可能避免它。第六章介绍另外一种设计模式,它提倡干净并且不鼓励遮蔽。
遮蔽甚至会以微妙的方式隐含地发生,因此要想避免它必须当心。考虑这段代码:
虽然看起来myObject.a++应当(经过委托)查询并 原地 递增anotherObject.a属性,可是++操做符至关于myObject.a = myObject.a + 1。结果就是在[[Prototype]]上进行a的[[Get]]查询,从anotherObject.a获得当前的值2,将这个值递增1,而后将值3用[[Put]]赋值到myObject上的新遮蔽属性a上。噢!
修改你的委托属性时要很是当心。若是你想递增anotherObject.a, 那么惟一正确的方法是anotherObject.a++。
“类”
如今你可能会想知道:“为何 一个对象须要链到另外一个对象?”真正的好处是什么?这是一个很恰当的问题,但在咱们可以彻底理解和体味它是什么和如何有用以前,咱们必须首先理解[[Prototype]] 不是 什么。
正如咱们在第四章讲解的,在JavaScript中,对于对象来讲没有抽象模式/蓝图,即没有面向类的语言中那样的称为类的东西。JavaScript 只有 对象。
实际上,在全部语言中,JavaScript 几乎是独一无二的,也许是惟一的能够被称为“面向对象”的语言,由于能够根本没有类而直接建立对象的语言不多,而JavaScript就是其中之一。
在JavaScript中,类不能(由于根本不存在)描述对象能够作什么。对象直接定义它本身的行为。这里 仅有 对象。
“类”函数
在JavaScript中有一种奇异的行为被无耻地滥用了许多年来 山寨 成某些 看起来 像“类”的东西。咱们来仔细看看这种方式。
“某种程度的类”这种奇特的行为取决于函数的一个奇怪的性质:全部的函数默认都会获得一个公有的,不可枚举的属性,称为prototype,它能够指向任意的对象。
这个对象常常被称为“Foo的原型”,由于咱们经过一个不幸地被命名为Foo.prototype的属性引用来访问它。然而,咱们立刻会看到,这个术语命中注定地将咱们搞糊涂。为了取代它,我将它称为“之前被认为是Foo的原型的对象”。只是开个玩笑。“一个被随意标记为‘Foo点儿原型’的对象”,怎么样?
无论咱们怎么称呼它,这个对象究竟是什么?
解释它的最直接的方法是,每一个由调用new Foo()(见第二章)而建立的对象将最终(有些随意地)被[[Prototype]]连接到这个“Foo点儿原型”对象。
让咱们描绘一下:
当经过调用new Foo()建立a时,会发生的事情之一(见第二章了解全部 四个 步骤)是,a获得一个内部[[Prototype]]连接,此连接链到Foo.prototype所指向的对象。
停一会来思考一下这句话的含义。
在面向类的语言中,能够制造一个类的多个 拷贝(即“实例”),就像从模具中冲压出某些东西同样。咱们在第四章中看到,这是由于初始化(或者继承)类的处理意味着,“将行为计划从这个类拷贝到物理对象中”,对于每一个新实例这都会发生。
可是在JavaScript中,没有这样的拷贝处理发生。你不会建立类的多个实例。你能够建立多个对象,它们的[[Prototype]]链接至一个共通对象。但默认地,没有拷贝发生,如此这些对象彼此间最终不会彻底分离和切断关系,而是 连接在一块儿。
new Foo()获得一个新对象(咱们叫他a),这个新对象a内部地被[[Prototype]]连接至Foo.prototype对象。
结果咱们获得两个对象,彼此连接。 如是而已。咱们没有初始化一个对象。固然咱们也没有作任何从一个“类”到一个实体对象拷贝。咱们只是让两个对象互相连接在一块儿。
事实上,这个使大多数JS开发者没法理解的秘密,是由于new Foo()函数调用实际上几乎和创建连接的处理没有任何 直接 关系。它是某种偶然的反作用。new Foo()是一个间接的,迂回的方法来获得咱们想要的:一个被连接到另外一个对象的对象。
咱们能用更直接的方法获得咱们想要的吗?能够! 这位英雄就是Object.create(..)。咱们过会儿就谈到它。
名称的意义何在?
在JavaScript中,咱们不从一个对象(“类”)向另外一个对象(“实例”) 拷贝。咱们在对象之间制造 连接。对于[[Prototype]]机制,视觉上,箭头的移动方向是从右至左,由下至上。
这种机制常被称为“原型继承(prototypal inheritance)”(咱们很快就用代码说明),它常常被说成是动态语言版的“类继承”。这种说法试图创建在面向类世界中对“继承”含义的共识上。可是 弄拧(意思是:抹平) 了被理解语义,来适应动态脚本。
先入为主,“继承”这个词有很强烈的含义(见第四章)。仅仅在它前面加入“原型”来区别于JavaScript中 实际上几乎相反 的行为,使真相在泥泞般的困惑中沉睡了近二十年。
我想说,将“原型”贴在“继承”以前很大程度上搞反了它的实际意义,就像一只手拿着一个桔子,另外一手拿着一个苹果,而坚持说苹果是一个“红色的桔子”。不管我在它前面放什么使人困惑的标签,那都不会改变一个水果是苹果而另外一个是桔子的 事实。
更好的方法是直白地将苹果称为苹果——使用最准确和最直接的术语。这样能更容易地理解它们的类似之处和 许多不一样之处,由于咱们都对“苹果”的意义有一个简单的,共享的理解。
因为用语的模糊和歧义,我相信,对于解释JavaScript机制真正如何工做来讲,“原型继承”这个标签(以及试图错误地应用全部面向类的术语,好比“类”,“构造器”,“实例”,“多态”等)自己带来的 危害比好处多。
“继承”意味着 拷贝 操做,而JavaScript不拷贝对象属性(原生上,默认地)。相反,JS在两个对象间创建连接,一个对象实质上能够将对属性/函数的访问 委托 到另外一个对象上。对于描述JavaScript对象连接机制来讲,“委托”是一个准确得多的术语。
另外一个有时被扔到JavaScript旁边的术语是“差分继承”。它的想法是,咱们能够用一个对象与一个更泛化的对象的 不一样 来描述一个它的行为。好比,你要解释汽车是一种载具,与其从新描述组成一个通常载具的全部特色,不如只说它有4个轮子。
若是你试着想象,在JS中任何给定的对象都是经过委托可用的全部行为的总和,并且 在你思惟中你扁平化 全部的行为到一个有形的 东西 中,那么你就能够(八九不离十地)看到“差分继承”是如何自圆其说的。
但正如“原型继承”,“差分继承”假意使你的思惟模型比在语言中物理发生的事情更重要。它忽视了这样一个事实:对象B实际上不是一个差别结构,而是由一些定义好的特定性质,与一些没有任何定义的“漏洞”组成的。正是经过这些“漏洞”(缺乏定义),委托能够接管而且动态地用委托行为“填补”它们。
对象不是像“差分继承”的思惟模型所暗示的那样,原生默认地,经过拷贝 扁平化到一个单独的差别对象中。如此,对于描述JavaScript的[[Prototype]]机制如何工做来讲,“差分继承”就不是天然合理。
你 能够选择 偏向“差分继承”这个术语和思惟模型,这是我的口味的问题,可是不可否认这个事实:它 仅仅 符合你思惟中的主观过程,不是引擎的物理行为。
"构造器"(Constructors)
让咱们回到早先的代码:
究竟是什么致使咱们认为Foo是一个“类”?
其一,咱们看到了new关键字的使用,就像面向类语言中人们构建类的对象那样。另外,它看起来咱们事实上执行了一个类的 构造器 方法,由于Foo()其实是个被调用的方法,就像当你初始化一个真实的类时这个类的构造器被调用的那样。
为了使“构造器”的语义更令人糊涂,被随意贴上标签的Foo.prototype对象还有另一招。考虑这段代码:
Foo.prototype对象默认地(就在代码段中第一行中声明的地方!)获得一个公有的,称为.constructor的不可枚举(见第三章)属性,并且这个属性回头指向这个对象关联的函数(这里是Foo)。另外,咱们看到被“构造器”调用new Foo()建立的对象a 看起来 也拥有一个称为.constructor的属性,也类似地指向“建立它的函数”。
注意: 这实际上不是真的。a上没有.constructor属性,而a.constructor确实解析成了Foo函数,“constructor”并不像它看起来的那样实际意味着“被XX建立”。咱们很快就会解释这个奇怪的地方。
哦,是的,另外……根据JavaScript世界中的惯例,“类”都以大写字母开头的单词命名,因此使用Foo而不是foo强烈地意味着咱们打算让它成为一个“类”。这对你来讲太明显了,对吧!?
注意: 这个惯例是如此强大,以致于若是你在一个小写字母名称的方法上使用new调用,或并无在一个大写字母开头的函数上使用new,许多JS语法检查器将会报告错误。这是由于咱们如此努力地想要在JavaScript中将(假的)“面向类” 搞对,因此咱们创建了这些语法规则来确保咱们使用了大写字母,即使对JS引擎来说,大写字母根本没有 任何意义。
构造器仍是调用?
上面的代码的段中,咱们试图认为Foo是一个“构造器”,是由于咱们用new调用它,并且咱们观察到它“构建”了一个对象。
在现实中,Foo不会比你的程序中的其余任何函数“更像构造器”。函数自身 不是 构造器。可是,当你在普通函数调用前面放一个new关键字时,这就将函数调用变成了“构造器调用”。事实上,new在某种意义上劫持了普通函数并将它以另外一种方式调用:构建一个对象,外加这个函数要作的其余任何事。
举个例子:
NothingSpecial仅仅是一个普通的函数,但当用new调用时,几乎是一种反作用,它会 构建 一个对象,并被咱们赋值到a。这个 调用 是一个 构造器调用,可是NothingSpecial自己并非一个 构造器。
换句话说,在JavaScript中,更合适的说法是,“构造器”是在前面 用new关键字调用的任何函数。
函数不是构造器,可是当且仅当new被使用时,函数调用是一个“构造器调用”。
机制
仅仅是这些缘由使得JavaScript中关于“类”的讨论变得命运多舛吗?
不全是。 JS开发者们努力地尽量的模拟面向类:
这段代码展现了另外两种“面向类”的花招:
this.name = name:在每一个对象(分别在a和b上;参照第二章关于this绑定的内容)上添加了.name属性,和类的实例包装数据值很类似。
Foo.prototype.myName = ...:这也许是更有趣的技术,它在Foo.prototype对象上添加了一个属性(函数)。如今,也许让人惊奇,a.myName()能够工做。可是是如何工做的?
在上面的代码段中,有很强的倾向认为当a和b被建立时,Foo.prototype上的属性/函数被 拷贝 到了a与b俩个对象上。可是,这没有发生。
在本章开头,咱们解释了[[Prototype]]链,和它做为默认的[[Get]]算法的一部分,如何在不能直接在对象上找到属性引用时提供后备的查询步骤。
因而,得益于他们被建立的方式,a和b都最终拥有一个内部的[[Prototype]]连接链到Foo.prototype。当没法分别在a和b中找到myName时,就会在Foo.prototype上找到(经过委托,见第六章)。
复活"构造器"
回想咱们刚才对.constructor属性的讨论,怎么看起来a.constructor === Foo为true意味着a上实际拥有一个.constructor属性,指向Foo?不对。
这只是一种不幸的混淆。实际上,.constructor引用也 委托 到了Foo.prototype,它 刚好 有一个指向Foo的默认属性。
这 看起来 方便得可怕,一个被Foo构建的对象能够访问指向Foo的.constructor属性。但这只不过是安全感上的错觉。它是一个欢乐的巧合,几乎是误打误撞,经过默认的[[Prototype]]委托a.constructor 刚好 指向Foo。实际上.construcor意味着“被XX构建”这种注定失败的臆测会以几种方式来咬到你。
第一,在Foo.prototype上的.constructor属性仅当Foo函数被声明时才出如今对象上。若是你建立一个新对象,并用它替换函数默认的.prototype对象引用,这个新对象上将不会魔法般地获得.contructor。
考虑这段代码:
Object(..)没有“构建”a1,是吧?看起来确实是Foo()“构建了”它。许多开发者认为Foo()在执行构建,但当你认为“构造器”意味着“被XX构建”时,一切就都崩塌了,由于若是那样的话,a1.construcor应当是Foo,但它不是!
发生了什么?a1没有.constructor属性,因此它沿者[[Prototype]]链向上委托到了Foo.prototype。可是这个对象也没有.constructor(默认的Foo.prototype对象就会有!),因此它继续委托,此次轮到了Object.prototype,委托链的最顶端。那个 对象上确实拥有.constructor,它指向内建的Object(..)函数。
误解,消除。
固然,你能够把.constructor加回到Foo.prototype对象上,可是要作一些手动工做,特别是若是你想要它与原生的行为吻合,并不可枚举时(见第三章)。
举例来讲:
要修复.constructor要花很多功夫。并且,咱们作的一切是为了延续“构造器”意味着“被XX构建”的误解。这是一种昂贵的假象。
事实上,一个对象上的.construcor默认地随意指向一个函数,而这个函数反过来拥有一个指向被这个对象称为.prototype的对象。“构造器”和“原型”这两个词仅有松散的默认含义,多是真的也可能不是真的。最佳方案是提醒你本身,“构造器不是意味着被XX构建”。
.constructor不是一个魔法般不可变的属性。它是不可枚举的(见上面的代码段),可是它的值是可写的(能够改变),并且,你能够在[[Prototype]]链上的任何对象上添加或覆盖(有意或无心地)名为constructor的属性,用你感受合适的任何值。
根据[[Get]]算法如何遍历[[Prototype]]链,在任何地方找到的一个.constructor属性引用解析的结果可能与你指望的十分不一样。
看到它的实际意义有多随便了吗?
结果?某些像a1.constructor这样随意的对象属性引用实际上不能被认为是默认的函数引用。还有,咱们立刻就会看到,经过一个简单的省略,a1.constructor能够最终指向某些使人诧异,没道理的地方。
a1.constructor是极其不可靠的,在你的代码中不该依赖的不安全引用。通常来讲,这样的引用应当尽可能避免。
“(原型)继承”
咱们已经看到了一些近似的“类”机制骇进JavaScript程序。可是若是咱们没有一种近似的“继承”,JavaScript的“类”将会更空洞。
实际上,咱们已经看到了一个常被称为“原型继承”的机制如何工做:a能够“继承自”Foo.prototype,并所以能够访问myName()函数。可是咱们传统的想法认为“继承”是两个“类”间的关系,而非“类”与“实例”的关系。
回想以前这幅图,它不只展现了从对象(也就是“实例”)a1到对象Foo.prototype的委托,并且从Bar.prototype到Foo.prototype,这酷似类继承的亲自概念。酷似,除了方向,箭头表示的是委托连接,而不是拷贝操做。
这里是一段典型的建立这样的连接的“原型风格”代码:
注意: 要想知道为何上面代码中的this指向a,参见第二章。
重要的部分是Bar.prototype = Object.create( Foo.prototype )。Object.create(..)凭空 建立 了一个“新”对象,并将这个新对象内部的[[Prototype]]连接到你指定的对象上(在这里是Foo.prototype)。
换句话说,这一行的意思是:“作一个 新的 连接到‘Foo点儿prototype’的‘Bar点儿prototype’对象”。
当function Bar() { .. }被声明时,就像其余函数同样,拥有一个链到默认对象的.prototype连接。可是 那个 对象没有链到咱们但愿的Foo.prototype。因此,咱们建立了一个 新 对象,链到咱们但愿的地方,并将原来的错误连接的对象扔掉。
注意: 这里一个常见的误解/困惑是,下面两种方法 也 能工做,可是他们不会如你指望的那样工做:
Bar.prototype = Foo.prototype不会建立新对象让Bar.prototype连接。它只是让Bar.prototype成为Foo.prototype的另外一个引用,将Bar直接链到Foo链着的 同一个对象:Foo.prototype。这意味着当你开始赋值时,好比Bar.prototype.myLabel = ...,你修改的 不是一个分离的对象 而是那个被分享的Foo.prototype对象自己,它将影响到全部连接到Foo.prototype的对象。这几乎能够肯定不是你想要的。若是这正是你想要的,那么你根本就不须要Bar,你应当仅使用Foo来使你的代码更简单。
Bar.prototype = new Foo()确实 建立了一个新的对象,这个新对象也的确连接到了咱们但愿的Foo.prototype。可是,它是用Foo(..)“构造器调用”来这样作的。若是这个函数有任何反作用(好比logging,改变状态,注册其余对象,向this添加数据属性,等等),这些反作用就会在连接时发生(并且极可能是对错误的对象!),而不是像可能但愿的那样,仅最终在Bar()的“后裔”被建立时发生。
因而,咱们剩下的选择就是使用Object.create(..)来制造一个新对象,这个对象被正确地连接,并且没有调用Foo(..)时所产生的反作用。一个轻微的缺点是,咱们不得不建立新对象,并把旧的扔掉,而不是修改提供给咱们的默认既存对象。
若是有一种标准且可靠地方法来修改既存对象的连接就行了。ES6以前,有一个非标准的,并且不是彻底对全部浏览器通用的方法:经过能够设置的.__proto__属性。ES6中增长了Object.setPrototypeOf(..)辅助工具,它提供了标准且可预见的方法。
让咱们一对一地比较ES6以前和ES6标准的技术如何处理将Bar.prototype连接至Foo.prototype:
若是忽略Object.create(..)方式在性能上的轻微劣势(扔掉一个对象,而后被回收),其实它相对短一些并且可能比ES6+的方式更易读。但两种方式可能都只是语法表面现象。
考察“类”关系
若是你有一个对象a而且但愿找到它委托至哪一个对象呢(若是有的话)?考察一个实例(一个JS对象)的继承血统(在JS中是委托连接),在传统的面向类环境中称为 自省(introspection)(或 反射(reflection))。
考虑下面的代码:
那么咱们如何自省a来找到它的“祖先”(委托链)呢?一种方式是接受“类”的困惑:
instanceof操做符的左边操做数接收一个普通对象,右边操做数接收一个 函数。instanceof回答的问题是:在a的整个[[Prototype]]链中,有没有出现被那个被Foo.prototype所随便指向的对象?
不幸的是,这意味着若是你拥有能够用于测试的 函数(Foo,和它带有的.prototype引用),你只能查询某些对象(a)的“祖先”。若是你有两个任意的对象,好比a和b,并且你想调查是否 这些对象 经过[[Prototype]]链相互关联,单靠instanceof帮不上什么忙。
注意: 若是你使用内建的.bind(..)工具来制造一个硬绑定的函数(见第二章),这个被建立的函数将不会拥有.prototype属性。将instanceof与这样的函数一块儿使用时,将会透明地替换为建立这个硬绑定函数的 目标函数 的.prototype。
将硬绑定函数用于“构造器调用”十分罕见,但若是你这么作,它会表现得好像是 目标函数 被调用了,这意味着将instanceof与硬绑定函数一块儿使用也会参照原版函数。
下面这段代码展现了试图经过“类”的语义和instanceof来推导 两个对象 间的关系是多么荒谬:
在isRelatedTo(..)内部,咱们借用一个一次性的函数F,从新对它的.prototype赋值,使他随意地指向某个对象o2,以后问是否o1是F的“一个实例”。很明显,o1实际上不是继承或遗传自F,甚至不是由F构建的,因此显而易见这种实践是愚蠢且让人困惑的。这个问题归根结底是将类的语义强加于JavaScript的尴尬,在这个例子中是由instanceof的间接语义揭露的。
第二种,也是更干净的方式,[[Prototype]]反射:
注意在这种状况下,咱们并不真正关心(甚至 不须要)Foo,咱们仅须要一个 对象(在咱们的例子中就是随意标志为Foo.prototype)来与另外一个 对象 测试。isPrototypeOf(..)回答的问题是:在a的整个[[Prototype]]链中,Foo.prototype出现过吗?
一样的问题,和彻底一样的答案。可是在第二种方式中,咱们实际上不须要间接地引用一个.prototype属性将被自动查询的 函数(Foo)。
咱们 只须要 两个 对象 来考察它们之间的关系。好比:
注意,这种方法根本不要求有一个函数(“类”)。它仅仅使用对象的直接引用b和c,来查询他们的关系。换句话说,咱们上面的isRelatedTo(..)工具是内建在语言中的,它的名字叫isPrototypeOf(..)。
咱们也能够直接取得一个对象的[[Prototype]]。在ES5中,这么作的标准方法是:
并且你将注意到对象引用是咱们指望的:
大多数浏览器(不是所有!)还一种长期支持的,非标准方法能够访问内部的[[Prototype]]:
这个奇怪的.__proto__(直到ES6才标准化!)属性“魔法般地”取得一个对象内部的[[Prototype]]做为引用,若是你想要直接考察(甚至遍历:.__proto__.__proto__...)[[Prototype]]链,这个引用十分有用。
和咱们早先看到的.constructor同样,.__proto__实际上不存在于你考察的对象上(在咱们的例子中是a)。事实上,它存在于(不可枚举地;见第二章)内建的Object.prototype上,和其余的共通工具在一块儿(.toString(), .isPrototypeOf(..), 等等)。
并且,.__proto__看起来像一个属性,但实际上将它看作是一个getter/setter(见第三章)更合适。
大体地,咱们能够这样描述.__proto__实现(见第三章,对象属性的定义):
因此,当咱们访问a.__proto__(取得它的值)时,就好像调用a.__proto__()(调用getter函数)。虽然getter函数存在于Object.prototype上(参照第二章,this绑定规则),但这个函数调用将a用做它的this,因此它至关于在说Object.getPrototypeOf( a )。
.__proto__仍是一个可设置的属性,就像早先展现过的ES6Object.setPrototypeOf(..)。然而,通常来讲你 不该该改变一个既存对象的[[Prototype]]。
在某些容许对Array定义“子类”的框架中,深度地使用了一些很是复杂,高级的技术,可是在通常的编程实践中常常是让人皱眉头的,由于这一般致使很是难理解/维护的代码。
注意: 在ES6中,关键字class将容许某些近似方法,对像Array这样的内建类型“定义子类”。参见附录A中关于ES6中加入的class的讨论。
仅有一小部分例外(就像前面提到过的),会设置一个默认函数.prototype对象的[[Prototype]],使它引用其余的对象(Object.prototype以外的对象)。它们会避免将这个默认对象彻底替换为一个新的连接对象。不然,为了在之后更容易地阅读你的代码 最好将对象的[[Prototype]]连接做为只读性质对待。
注意: 针对双下划线,特别是在像__proto__这样的属性中开头的部分,JavaScript社区非官方地创造了一个术语:“dunder”。因此,那些JavaScript的“酷小子”们一般将__proto__读做“dunder proto”。
对象连接
正如咱们看到的,[[Prototype]]机制是一个内部连接,它存在于一个对象上,这个对象引用一些其余的对象。
这种连接(主要)在对第一个对象进行属性/方法引用,但这样的属性/方法不存在时实施。在这种状况下,[[Prototype]]连接告诉引擎在那个被连接的对象上查找这个属性/方法。接下来,若是这个对象不能知足查询,它的[[Prototype]]又会被查找,如此继续。这个在对象间的一系列连接构成了所谓的“原形链”。
建立连接
咱们已经完全揭露了为何JavaScript的[[Prototype]]机制和 类 不 同样,并且咱们也看到了如何在正确的对象间建立 连接。
[[Prototype]]机制的意义是什么?为何老是见到JS开发者们费那么大力气(模拟类)在他们的代码中搞乱这些连接?
记得咱们在本章很靠前的地方说过Object.create(..)是英雄吗?如今,咱们准备好看看为何了。
Object.create(..)建立了一个连接到咱们指定的对象(foo)上的新对象(bar),这给了咱们[[Prototype]]机制的全部力量(委托),并且没有new函数做为类和构造器调用产生的任何不必的复杂性,搞乱.prototype和.constructor 引用,或任何其余的多余的东西。
注意: Object.create(null)建立一个拥有空(也就是null)[[Prototype]]连接的对象,如此这个对象不能委托到任何地方。由于这样的对象没有原形链,instancof操做符(前面解释过)没有东西可检查,因此它总返回false。因为他们典型的用途是在属性中存储数据,这种特殊的空[[Prototype]]对象常常被称为“dictionaries(字典)”,这主要是由于它们没有可能受到在[[Prototype]]链上任何委托属性/函数的影响,因此它们是纯粹的扁平数据存储。
咱们不 须要 类来在两个对象间建立有意义的关系。咱们须要 真正关心 的惟一问题是对象为了委托而连接在一块儿,而Object.create(..)给咱们这种连接而且没有一切关于类的烂设计。
填补Object.create()
Object.create(..)在ES5中被加入。你可能须要支持ES5以前的环境(好比老版本的IE),因此让咱们来看一个Object.create(..)的简单 部分 填补工具,它甚至能在更老的JS环境中给咱们所需的能力:
这个填补工具经过一个一次性的F函数并覆盖它的.prototype属性来指向咱们想链接到的对象。以后咱们用new F()构造器调用来制造一个将会链到咱们指定对象上的新对象。
Object.create(..)的这种用法是目前最多见的用法,由于他的这一部分是 能够 填补的。ES5标准的内建Object.create(..)还提供了一个附加的功能,它是 不能 被ES5以前的版本填补的。如此,这个功能的使用远没有那么常见。为了完整性,让我么看看这个附加功能:
Object.create(..)的第二个参数指定了要添加在新对象上的属性名,经过声明每一个新属性的 属性描述符(见第三章)。由于在ES5以前的环境中填补属性描述符是不可能的,因此Object.create(..)的这个附加功能没法填补。
由于Object.create(..)的绝大多数用途都是使用填补安全的功能子集,因此大多数开发者在ES5以前的环境中使用这种 部分填补 也没有问题。
有些开发者采起严格得多的观点,也就是除非可以被 彻底 填补,不然没有函数应该被填补。由于Object.create(..)能够部分填补的工具之一,这种较狭窄的观点会说,若是你须要在ES5以前的环境中使用Object.create(..)的任何功能,你应当使用自定义的工具,而不是填充,并且应当完全远离使用Object.create这个名字。你能够定义本身的工具,好比:
我不会分享这种严格的观点。我彻底拥护如上面展现的Object.create(..)的常见部分填补,甚至在ES5以前的环境下在你的代码中使用它。我将选择权留给你。
连接做为候补?
也许这么想很吸引人:这些对象间的连接 主要 是为了给“缺失”的属性和方法提供某种候补。虽然这是一个可观察到的结果,可是我不认为这是考虑[[Prototype]]的正确方法。
考虑下面的代码:
得益于[[Prototype]],这段代码能够工做,但若是你这样写是为了 万一 myObject不能处理某些开发者可能会调用的属性/方法,而让anotherObject做为一个候补,你的软件大概会变得有点儿“魔法”而且更难于理解和维护。
这不是说候补在任何状况下都不是一个合适的设计模式,但它不是一个在JS中很常见的用法,因此若是你发现本身在这么作,那么你可能想要退一步并从新考虑它是否真的是合适且合理的设计。
注意: 在ES6中,引入了一个称为Proxy(代理)的高级功能,它能够提供某种“方法未找到”类型的行为。Proxy超出了本书的范围,但会在之后的 “你不懂JS” 系列图书中详细讲解。
这里不要错过一个重要的细节。
例如,你打算为一个开发者设计软件,若是即便在myObject上没有cool()方法时调用myObject.cool()也能工做,会在你的API设计上引入一些“魔法”气息,这可能会使将来维护你的软件的开发者很吃惊。
然而你能够在你的API设计上少用些“魔法”,而仍然利用[[Prototype]]连接的力量。
这里,咱们调用myObject.doCool(),它是一个 实际存在于 myObject上的方法,这使咱们的API设计更清晰(没那么“魔法”)。在它内部,咱们的实现依照 委托设计模式(见第六章),利用[[Prototype]]委托到anotherObject.cool()。
换句话说,若是委托是一个内部实现细节,而非在你的API结构设计中简单地暴露出来,它倾向于减小意外/困惑。咱们会在下一章中详细解释 委托。
复习
当试图在一个对象上进行属性访问,而对象没有该属性时,对象内部的[[Prototype]]连接定义了[[Get]]操做(见第三章)下一步应当到哪里寻找它。这种对象到对象的串行连接定义了对象的“原形链”(和嵌套的做用域链有些类似),在解析属性时发挥做用。
全部普通的对象用内建的Object.prototype做为原形链的顶端(就像做用域查询的顶端是全局做用域),若是属性没能在链条的前面任何地方找到,属性解析就会在这里中止。toString(),valueOf(),和其余几种共同工具都存在于这个Object.prototype对象上,这解释了语言中全部的对象是如何可以访问他们的。
使两个对象相互连接在一块儿的最多见的方法是将new关键字与函数调用一块儿使用,在它的四个步骤中(见第二章),就会创建一个新对象连接到另外一个对象。
那个用new调用的函数有一个被随便地命名为.prototype的属性,这个属性所引用的对象刚好就是这个新对象连接到的“另外一个对象”。带有new的函数调用一般被称为“构造器”,尽管实际上它们并无像传统的面相类语言那样初始化一个类。
虽然这些JavaScript机制看起来和传统面向类语言的“初始化类”和“类继承”相似,而在JavaScript中的关键区别是,没有拷贝发生。取而代之的是对象最终经过[[Prototype]]链连接在一块儿。
因为各类缘由,不光是前面提到的术语,“继承”(和“原型继承”)与全部其余的OO用语,在考虑JavaScript实际如何工做时都没有道理。
相反,“委托”是一个更确切的术语,由于这些关系不是 拷贝 而是委托 连接。