26.原型链和类
原文: http://exploringjs.com/impatient-js/ch_proto-chains-classes.html
在本书中,JavaScript 的面向对象编程(OOP)风格分四步介绍。本章包括步骤 2-4,前一章涵盖步骤 1.步骤为(图 8 ):
- 单个对象: 对象 (JavaScript 的基本 OOP 构建块)如何独立工作?
- 原型链:每个对象都有一个零个或多个 原型对象链 。原型是 JavaScript 的核心继承机制。
- 类: JavaScript 的 类 是对象的工厂。类及其实例之间的关系基于原型继承。
- 子类化: 亚类 与其 超类 之间的关系也是基于原型继承。
Figure 8: This book introduces object-oriented programming in JavaScript in four steps.
26.1。原型链
原型是 JavaScript 唯一的继承机制:每个对象都有一个原型,它是null
或一个对象。在后一种情况下,对象继承了所有原型的属性。
在对象字面值中,您可以通过特殊属性__proto__
设置原型:
鉴于原型对象本身可以拥有原型,我们得到了一系列对象 - 所谓的 原型链 。这意味着继承给我们的印象是我们正在处理单个对象,但实际上我们处理的是对象链。
图 9 显示obj
的原型链是什么样的。
Figure 9: obj
starts a chain of objects that continues with proto
and other objects.
非继承属性称为 自己的属性 。 obj
有一个属性.objProp
。
26.1.1。陷阱:只有原型链的第一个成员发生了变异
可能违反直觉的原型链的一个方面是通过对象设置 任何 属性 - 甚至是继承的 - 仅改变该对象 - 从不是原型之一。
考虑以下对象obj
:
当我们在行 A 中设置继承属性obj.protoProp
时,我们通过创建自己的属性来“更改”它:当读取obj.protoProp
时,首先找到自己的属性,它的值将覆盖继承属性的值。
obj
的原型链如图 2 所示。 10 。
Figure 10: The own property .protoProp
of obj
overrides the property inherited from proto
.
26.1.2。使用原型的提示(高级)
26.1.2.1。避免__proto__
(除了对象字面值)
我建议避免使用特殊属性__proto__
:它是通过Object.prototype
中的 getter 和 setter 实现的,因此只有在Object.prototype
位于对象的原型链中时才可用。通常情况就是如此,但为了安全起见,您可以使用以下替代方案:
设置原型的最佳方法是创建对象。例如。通过:
如果必须,可以使用
Object.setPrototypeOf()
更改现有对象的原型。获取原型的最佳方法是通过以下方法:
以下是这些功能的使用方法:
请注意,对象字面值中的__proto__
是不同的。在那里,它是一个内置功能,总是安全使用。
26.1.2.2。检查:对象是另一个的原型吗?
更宽松的定义“o
是p
的原型”是“o
在p
的原型链中”。可以通过以下方式检查此关系:
例如:
26.1.3。通过原型共享数据
请考虑以下代码:
我们有两个非常相似的对象。两者都有两个属性,其名称为.name
和.describe
。另外,方法.describe()
是相同的。我们怎样才能避免重复该方法?
我们可以将它移动到共享原型,PersonProto
:
原型的名称反映出jane
和tarzan
都是人。
Figure 11: Objects jane
and tarzan
share method .describe()
, via their common prototype PersonProto
.
图中的图表 11 说明了三个对象是如何连接的:底部的对象现在包含特定于jane
和tarzan
的属性。顶部的对象包含它们之间共享的属性。
当您调用方法jane.describe()
时,this
指向该方法调用的接收者,jane
(在图的左下角)。这就是该方法仍然有效的原因。当你打调用给tarzan.describe()
时会发生类似的事情。
26.2。类
我们现在准备接受类,这基本上是用于设置原型链的紧凑语法。虽然他们的基础可能是非常规的,但是如果您以前使用过面向对象的语言,那么使用 JavaScript 的类仍然应该感觉很熟悉。
26.2.1。一类人
我们之前使用过jane
和tarzan
,代表人物的单个对象。让我们用一个班来为人们实施一个工厂:
现在可以通过new Person()
创建jane
和tarzan
:
26.2.2。类表达式
前一个类定义是 类声明 。还有 匿名类表达式 :
并且 命名了类表达式 :
26.2.3。引擎盖下的课程(高级)
在课程的引擎下有很多事情要做。让我们看一下jane
的图表(图 12 )。
Figure 12: The class Person
has the property .prototype
that points to an object that is the prototype of all instances of Person
. jane
is one such instance.
类Person
的主要目的是在右侧设置原型链(jane
,然后是Person.prototype
)。值得注意的是,类Person
(.constructor
和.describe()
)内的两个构造都为Person.prototype
创建了属性,而不是Person
。
这种稍微奇怪的方法的原因是向后兼容性:在类之前, 构造函数 (普通函数,通过new
运算符调用)通常用作对象的工厂。类通常是构造函数的更好语法,因此与旧代码保持兼容。这解释了为什么类是函数:
在本书中,我可以互换地使用术语 构造函数(函数) 和 类 。
很多人混淆.__proto__
和.prototype
。希望图中的图表。 12 清楚说明了它们的区别:
.__proto__
是用于访问对象原型的特殊属性。.prototype
是一个普通的属性,由于new
操作符的使用方式,它只是特殊的。名称并不理想:Person.prototype
没有指向Person
的原型,它指向Person
的所有实例的原型。
26.2.3.1。 Person.prototype.constructor
图中有一个细节。 12 我们尚未查看,但是:Person.prototype.constructor
指回Person
:
由于历史原因,此设置也存在。但它也有两个好处。
首先,类的每个实例都继承属性.constructor
。因此,给定一个实例,您可以通过它创建“类似”对象:
其次,您可以获取创建给定实例的类的名称:
26.2.4。类定义:原型属性
以下代码演示了创建Foo.prototype
属性的类定义Foo
的所有部分:
让我们按顺序检查它们:
- 在创建
Foo
的新实例后调用.constructor()
来设置该实例。 .protoMethod()
是一种常规方法。它存储在Foo.prototype
中。.protoGetter
是存储在Foo.prototype
中的吸气剂。
以下交互使用类Foo
:
26.2.5。类定义:静态属性
下面的代码演示了类定义的所有部分,它们创建了所谓的 静态属性 - 类本身的属性。
静态方法和静态吸气剂使用如下。
26.2.6。 instanceof
运算符
instanceof
运算符告诉您某个值是否是给定类的实例:
在我们查看子类化之后,我们将在后面中更详细地探索instanceof
运算符。
26.2.7。为什么我推荐课程
我推荐使用类,原因如下:
- 类是对象创建和继承的通用标准,现在跨框架(React,Angular,Ember 等)广泛支持。
- 他们帮助 IDE 和类型检查器等工具完成工作并启用新功能。
- 它们是未来功能的基础,例如值对象,不可变对象,装饰器等。
- 它们使新手更容易开始使用 JavaScript。
- JavaScript 引擎优化它们。也就是说,使用类的代码通常比使用自定义继承库的代码更快。
这并不意味着课程是完美的。我和他们有一个问题是:
- 课程看起来与他们在幕后的不同。换句话说,语法和语义之间存在脱节。
如果类是(语法)构造函数 对象 (new
- 原型对象)而不是构造函数 函数 ,那将是很好的。但后向兼容性是他们成为后者的正当理由。
练习:实现一个类
exercises/proto-chains-classes/point_class_test.js
26.3。类的私有数据
本节描述了从外部隐藏对象的一些数据的技术。我们在类的上下文中讨论它们,但它们也适用于通过对象字面值等直接创建的对象。
26.3.1。私有数据:命名约定
第一种技术通过在其名称前加下划线来使属性成为私有属性。这不会以任何方式保护财产;它只是向外界发出信号:“你不需要知道这个房产。”
在以下代码中,属性._counter
和._action
是私有的。
使用这种技术,您不会得到任何保护,私人名称可能会发生冲突。从好的方面来说,它很容易使用。
26.3.2。私人数据:WeakMaps
另一种技术是使用 WeakMaps。在关于 WeakMaps 的章节中解释了究竟是如何工作的。这是预览:
这种技术为您提供了相当大的外部访问保护,并且不会有任何名称冲突。但使用起来也更复杂。
26.3.3。更多私人数据技术
类的私有数据有更多技术。这些在“探索 ES6”中进行了解释。
本节没有深入探讨的原因是 JavaScript 可能很快就会内置对私有数据的支持。请参阅 ECMAScript 提案“类公共实例字段&私有实例字段“了解详情。
26.4。子类
类也可以子类化(“扩展”)现有类。例如,以下类Employee
子类Person
:
两条评论:
在
.constructor()
方法中,必须先通过super()
调用超级构造函数,然后才能访问this
。那是因为在调用超级构造函数之前this
不存在(这种现象特定于类)。静态方法也是继承的。例如,
Employee
继承静态方法.logNames()
:
练习:子类化
exercises/proto-chains-classes/color_point_class_test.js
26.4.1。引擎盖下的子类(高级)
Figure 13: These are the objects that make up class Person
and its subclass, Employee
. The left column is about classes. The right column is about the Employee
instance jane
and its prototype chain.
上一节中的Person
和Employee
类由几个对象组成(图 13 )。理解这些对象如何相关的一个关键见解是,有两个原型链:
- 实例原型链,在右侧。
- 类原型链,在左边。
26.4.1.1。实例原型链(右栏)
实例原型链以jane
开始,并继续Employee.prototype
和Person.prototype
。原则上,原型链在此时结束,但我们还得到一个对象:Object.prototype
。这个原型为几乎所有对象提供服务,这也是为什么它包含在这里:
26.4.1.2。类原型链(左栏)
在类原型链中,Employee
首先出现,Person
接下来。之后,链继续Function.prototype
,只有那里,因为Person
是一个功能,功能需要Function.prototype
的服务。
26.4.2。 instanceof
更详细(高级)
我们还没有看到instanceof
如何真正起作用。给定表达式x instanceof C
,instanceof
如何确定x
是否是C
的实例?它通过检查C.prototype
是否在x
的原型链中来实现。也就是说,以下两个表达式是等效的:
如果我们回到图。 13 ,我们可以确认原型链确实引导我们得到以下答案:
26.4.3。内置对象的原型链(高级)
接下来,我们将使用我们的子类化知识来理解一些内置对象的原型链。以下工具功能p()
帮助我们进行探索。
我们提取Object
的方法.getPrototypeOf()
并将其分配给p
。
26.4.3.1。 {}
的原型链
让我们从检查普通对象开始:
Figure 14: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype
and ends with null
.
图 14 显示了该原型链的图表。我们可以看到{}
确实是Object
的实例 - Object.prototype
在其原型链中。
Object.prototype
是一个奇怪的值:它是一个对象,但它不是Object
的实例:
这是无法避免的,因为Object.prototype
不能在自己的原型链中。
26.4.3.2。 []
的原型链
Array 的原型链是什么样的?
Figure 15: The prototype chain of an Array has these members: the Array instance, Array.prototype
, Object.prototype
, null
.
这个原型链(在图 15 中可视化)告诉我们一个 Array 对象是Array
的一个实例,它是Object
的子类。
26.4.3.3。 function () {}
的原型链
最后,普通函数的原型链告诉我们所有函数都是对象:
26.4.3.4。不是Object
实例的对象
如果Object.prototype
在其原型链中,则对象只是Object
的实例。通过各种字面值创建的大多数对象是Object
的实例:
没有原型的对象不是Object
的实例:
Object.prototype
结束了大多数原型链。它的原型是null
,这意味着它不是Object
的实例,也是:
26.4.4。调度与直接方法调用(高级)
让我们来看一下方法调用如何与类一起工作。我们从之前再次访问jane
:
图 16 有一个带有jane
原型链的图表。
Figure 16: The prototype chain of jane
starts with jane
and continues with Person.prototype
.
正常方法调用是 调度 。要使方法调用jane.describe()
:
- JavaScript 首先通过遍历原型链来查找
jane.describe
的值。 - 然后它调用它找到的函数,同时将
this
设置为jane
。this
是方法调用的 接收器 (其中搜索属性.describe
已启动)。
这种动态查找方法的方式称为 动态调度 。
您可以在绕过调度时进行相同的方法调用:
这次,Person.prototype.describe
是一个自己的属性,不需要搜索原型。我们还通过.call()
自己指定this
。
注意this
总是指向原型链的开头。这使.describe()
能够访问.name
。这是突变发生的地方(如果方法想要设置.name
)。
26.4.4.1。借用方法
使用Object.prototype
的方法时,直接方法调用很有用。例如,Object.prototype.hasOwnProperty()
检查对象是否具有其键为给定的非继承属性:
但是,可以覆盖此方法。然后调度的方法调用不起作用:
解决方法是使用直接方法调用:
这种直接方法调用通常缩写如下:
JavaScript 引擎优化了这种模式,因此性能不应成为问题。
26.4.5。 Mixin 课程(高级)
JavaScript 的类系统仅支持 单继承 。也就是说,每个类最多只能有一个超类。绕过这种限制的方法是通过称为 mixin 类 (简称: mixins )的技术。
这个想法如下:让我们假设有一个类C
扩展了一个类S
- 它的超类。 Mixins 是插入C
和S
之间的类片段。
在 JavaScript 中,您可以通过一个函数实现 mixin Mix
,该函数的输入是一个类,其输出是 mixin 类片段 - 一个扩展输入的新类。要使用Mix()
,请按如下方式创建C
。
我们来看一个例子:
我们使用这个 mixin 在Car
和Object
之间插入一个类:
以下代码确认 mixin 有效:Car
具有Branded
的方法.setBrand()
。
Mixins 比普通类更灵活:
首先,您可以在多个类中多次使用相同的 mixin。
其次,您可以同时使用多个 mixin。例如,考虑一个名为
Stringifiable
的附加 mixin,它有助于实现.toString()
。我们可以使用Branded
和Stringifiable
如下:
测验
参见测验应用程序。