JS对象:继承了混乱
JS对象:继承了混乱
JS对象:TL; DR
自一开始以来,JavaScript就一直困扰着其“原型继承”系统,人们对其产生了误解和尴尬,这主要是因为“继承”根本不是JS的工作原理,而试图做到这一点只会导致我们陷入困境和困惑。必须铺平用户土地的帮助库。 相反,认为JS具有“行为委托”(仅是对象之间的委托链接)自然符合JS语法的工作原理,从而无需帮助者即可创建更明智的代码。
当您搁置诸如混合,多态性,组成,类,构造函数和实例之类的分散注意力,而仅关注彼此链接的对象时,您将获得行为委托中的强大工具,该工具更易于编写,推理,解释,和代码维护。 越简单越好。 JS是“仅对象”(OO)。 将课程留给其他语言!
由于感谢
我要感谢以下出色的开发者在本文系列的反馈/技术评论中所花的大量时间:David Bruant,Hugh Wood,Mark Trostler和Mark McDonnell。 我也很荣幸David Walsh希望将这些文章发布在他的博客上。
完整系列
2013年:Haskell人员仍在编写monad教程,JavaScript人员仍在尝试解释继承。
— Vyacheslav Egorov(@mraleph) 2013年4月12日
引用那句话对JS进行的批评令人遗憾,这是真的。 (我对Haskell或Monads没有任何见解,所以我只在谈论JS和继承部分)。 所有的混乱和,取决于你的偏见,“坏”,这JS语言的部分,的行为, this
与[[Prototype]]
链一直保持一些最难以捉摸的解释和正确使用。
作为背景,我从2007年开始就全职开发JS。那时我的第一个主要顿悟是了解闭包如何工作以及它们如何启用经典模块模式。 我写的第一个开源项目(2008年初)是flXHR ,它是一个使用标准Ajax(XHR)接口(通过隐藏的Flash元素)的跨域Ajax prollyfill ,它很大程度上依赖于模块模式。
很可能是我的“啊哈!” 围绕模块模式的那一刻让我非常满意,以至于我从没真正感到非常需要将“继承”模式应用于我的JS设计。
不过,像大多数JS开发人员一样,多年来,我已经阅读了许多博客和书籍,这些博客和书籍试图(而且大多失败了)来解释“ JavaScript继承”(又名“原型继承”)的吸引力和奥秘。
但是,如果很难理解,甚至很难正确地进行正确操作,那么这一点仍然无法理解。 显然,我并不孤单。
JavaScript中的OO
在传统的面向对象语言中 , 类的语法与语义匹配。 您可以使用语言的语法直接明确地表达面向对象的类,继承和多态性的概念。 无需使用其他其他语言工具的变通办法,就可以使用一些帮助程序库来伪装成类似OO的行为。
另一方面,JavaScript具有一组看起来有些为OO的语法 ,但其行为方式却令人沮丧地不同(我们将在本系列文章中介绍)。 结果,在JS中实现OO模式的常见方式是通过多种用户土地帮助程序库中的任何一种,这些库可让您表达“对象”之间所需的语义关系。 大多数JS开发人员使用它们的原因是因为基本的JS语法使这些语义表达式尴尬。 只是让一个库来处理令人困惑的语法错误是很不错的。
jQuery之类的库非常有用,因为它们隐藏了处理JS引擎中跨浏览器差异的丑陋细节。 但是这些OO-helper库是不同的:它们将竭尽全力隐藏JavaScript OO机制的真实本质 ,而不是以其他语言更熟悉的一组模式来掩盖它们。
在理解这一点时,我们应该真正地问自己: 纯JavaScript中表达类和继承的困难是该语言的一种失败(可以通过 用户库 暂时 解决 ,最终可以通过添加诸如class { .. }
的语言来解决) class { .. }
语法),或者像许多开发人员所感觉到的更深? 这是否表明存在更根本的差异,我们正试图在JS中做一些本来不应该做的事情 ?
并非所有人都喝过JS类kool-aid,因此本系列文章的其余部分将倾向于不同的观点。
蓝图
传统类/继承OO中使用的最常见的隐喻之一是, 该类代表要建造房屋的“蓝图” ,但是实例化该类后,基本上就是将所有特征从蓝图复制到实际的建筑中。屋。 这个隐喻在某种程度上与代码在编译时在语言级别上实际发生的情况相匹配,这是因为它将某种类(没有“虚拟”方法)继承层次结构的定义整理到实例中。
当然, 面向继承的编码的主要Struts是重写和多态性 ,它允许对象自动访问方法的最后代定义,但也可以使用super
样式相对引用来访问祖先(也称为“虚拟”)版本。的同名方法。 在这些情况下,编译器会维护虚拟方法的查找表,但是会拼凑类/继承定义的非虚拟部分。 编译器可以确定哪些内容需要保留,哪些不需要保留,并可以高度优化其在编译代码中创建的定义结构。
就我们的目的而言,我们可以将传统的类继承视为从行为链到实例的扁平化“行为”副本。 这是说明父/基类Foo
和子类Bar
之间的继承关系的图,然后分别说明它们的实例,分别命名为foo1
, foo2
, bar1
和bar2
。 在视觉上,箭头(又称“复制”)从左至右和从上至下指向:
名字叫什么?
尽管借用了通用名称“原型继承”的含义,但JavaScript的机制却大不相同,我们稍后会看到。
无论从定义上 (“ ...从父母传给后代的特征”)还是从行为上(如上所述),“继承”都与从父母到子女“复制”的思想最为相关。
然后,当您采用“继承”并将其应用于具有某些截然不同的行为的机制时,您会遇到困扰“ JavaScript继承” 文档 , 培训和使用近20年的困惑。
为了解决这个混乱局面,让我们抛开标签“继承”及其对JS的含义,希望我们可以在概念上更准确,功能上更有用。
ABD的:总是委派
JavaScript的OO状属性机构,用于对象由谱写[[Prototype]]
这是任何对象的内部特性称为其原型链-一种特殊的链接到另一个对象。 这有点像作用域机制,因为[[Prototype]]
链接描述了如果您请求不存在的对象的属性或方法, 则应引用哪个替代对象 。
换句话说,如果没有在有关对象上定义行为,则表示要向其委派行为的对象。
上面的用JS表示的面向类的Foo
和Bar
示例将对象Bar.prototype
与Foo.prototype
Bar.prototype
,然后将foo1
, foo2
, bar1
和bar2
对象bar2
到它们各自的[[Prototype]]
s。 箭头(不是副本而是实时链接)在JS中从右到左,从下到上的方式指向:
“行为委托”是描述JavaScript的[[Prototype]]
的更准确的术语。 这不仅仅是单词语义的问题,还是一种根本不同的功能类型。
如果尝试用“蓝图”隐喻来说明行为委托,那么您很快就会发现它是如何分解的。 我的家里没有客人卧室,没有办法简单地参考另一所房子或原始设计图,以便为我婆婆上门时提供一间卧室。 尽管您可以实现的结果有一些相似之处,但是“继承”和“行为委托”的概念却截然不同 。
一些开发人员坚持认为,“委托”只是“继承”的动态版本,就像同一枚硬币的两个侧面一样,但我将它们视为正交系统 。
如何委托?
我们将在文章系列的后面部分再次讨论这一点,但是Object.create(..)
已添加到ES5中,以帮助创建对象,然后可选地将其[[Prototype]]
到另一个对象。 创建的链接是委托链接,而不是按副本继承。
注意:一旦对象在创建时设置了[[Prototype]]
链,就应将其大部分视为固定的,并且不可更改。 从技术上讲,支持__proto__
属性 (内部链接的公共表示)的浏览__proto__
您可以在链接对象的任何时间进行更改。 但是,这种做法到处都是地雷,而且人们普遍对此不以为然–几乎可以肯定,您希望在代码中避免这种情况。
铁锹铁锹
您已经了解到JavaScript中的机制与其他语言中的机制相比有何不同。 但是只需要手动放弃这些差异,这样我们就可以继续为JS使用术语“继承”吗?
事实是,这并不是该术语的准确用法。 通过坚持认为JavaScript具有“继承性”,我们实际上是在说“继承”一词的含义无关紧要,或者说很软。
JS不会静态分析它可以安全地展平和复制的继承链的哪些部分,而是在整个运行时将指向整个委派链的链接保持为不同的对象 ,这意味着我们的代码可以利用各种强大的“后期绑定”优势动态模式 。
如果我们继续尝试模仿JavaScript中的继承(该死的语法障碍),我们会分心 , 从一开始就错过了我们语言内置的所有功能 。
我说:让我们称之为它,然后停止尝试在JavaScript上堆积“继承”标签所暗示的其他概念。
所以呢?
到目前为止,我已经尝试找出一些关于JS的[[Prototype]]
机制以及“继承”不是有用标签的误解。
您可能仍然持怀疑态度,为什么它实际上很重要,我们在JS中称之为“类似于OO的机制”? 在本系列的下一部分中,我将解决许多传统的“基于类”编程的陷阱,我认为这些陷阱使我们无法了解JS对象如何进行互操作的本质。 实际上,我们甚至可以说类/继承是 JavaScript 的过早优化 。
清除这些干扰可以使我们进入第3部分 ,在该部分中 ,我们将看到一个更简单,更强大的JS代码模式,更重要的是, 我们的代码实际上将与我们的语义匹配, 而无需我们跳过箍来隐藏代码。丑陋的不匹配。
关于凯尔·辛普森
凯尔·辛普森(Kyle Simpson)是一位开放网络传播者,对JavaScript充满热情。 他是作家,讲习班培训师,技术发言人和OSS贡献者/负责人。