15. 类
- 15.1. 概述
- 15.2. 要点
- 15.2.1. 基类
- 15.2.2. 类定义的主体内部
- 15.2.3. 子类化
- 15.3. 类的私有数据
- 15.3.1. 通过构造函数环境实现私有数据
- 15.3.2. 通过命名约定实现私有数据
- 15.3.3. 通过 WeakMaps 实现私有数据
- 15.3.4. 通过符号实现私有数据
- 15.3.5. 扩展阅读
- 15.4. 简单的混入
- 15.5. 类的细节
- 15.5.1. 各种检查
- 15.5.2. 属性的特性
- 15.5.3. 类有内部名称
- 15.6. 子类化的细节
- 15.6.1. 原型链
- 15.6.2. 分配和初始化实例
- 15.6.3. 为什么不能在 ES5 中对内置构造函数进行子类化?
- 15.6.4. 在方法中引用超类属性
- 15.7. 物种模式
- 15.7.1. 示例的辅助方法
- 15.7.2. 标准物种模式
- 15.7.3. 数组的物种模式
- 15.7.4. 静态方法中的物种模式
- 15.7.5. 在子类中覆盖默认物种
- 15.8. 类的优缺点
- 15.8.1. 抱怨:ES6 类掩盖了 JavaScript 继承的本质
- 15.8.2. 抱怨:类只提供单继承
- 15.8.3. 抱怨:由于强制使用
new
,类将您锁定
- 15.9. 常见问题解答:类
- 15.9.1. 为什么不能函数调用类?
- 15.9.2. 给定一个参数数组,如何实例化一个类?
- 15.10. 类的未来发展方向是什么?
- 15.11. 扩展阅读
15.1 概述
一个类和一个子类
使用类
在底层,ES6 类并不是什么全新的东西:它们主要提供了更方便的语法来创建老式的构造函数。如果您使用 typeof
,就可以看到这一点
15.2 要点
15.2.1 基类
在 ECMAScript 6 中,类的定义如下
您可以像使用 ES5 构造函数一样使用此类
实际上,类定义的结果是一个函数
但是,您只能通过 new
调用类,而不能通过函数调用调用(其原因稍后解释)
15.2.1.1 类定义的成员之间没有分隔符
类定义的成员之间没有分隔标点符号。例如,对象字面量的成员之间用逗号分隔,这在类定义的顶层是非法的。允许使用分号,但会被忽略
允许使用分号是为了将来可能包含以分号结尾的成员的语法做准备。禁止使用逗号是为了强调类定义与对象字面量不同。
15.2.1.2 类声明不会被提升
函数声明会被*提升*:进入作用域时,其中声明的函数会立即可用,而与声明发生的位置无关。这意味着您可以调用稍后声明的函数
相反,类声明不会被提升。因此,类只有在执行到达其定义并对其进行求值后才存在。事先访问它会导致 ReferenceError
这种限制的原因是类可以有一个 extends
子句,其值为任意表达式。该表达式必须在其适当的“位置”进行求值,其求值不能被提升。
没有提升并不像您想象的那么受限。例如,在类声明之前出现的函数仍然可以引用该类,但您必须等到类声明被求值后才能调用该函数。
15.2.1.3 类表达式
与函数类似,有两种*类定义*,即定义类的两种方式:*类声明*和*类表达式*。
与函数表达式类似,类表达式可以是匿名的
同样与函数表达式类似,类表达式可以具有仅在内部可见的名称
最后两行表明 Me
在类外部不会成为变量,但可以在类内部使用。
15.2.2 类定义的主体内部
类主体只能包含方法,而不能包含数据属性。原型具有数据属性通常被认为是一种反模式,因此这只是强制执行最佳实践。
15.2.2.1 constructor
、静态方法、原型方法
让我们来看看您经常在类定义中发现的三种方法。
此类声明的对象图如下所示。理解它的提示:[[Prototype]]
是对象之间的继承关系,而 prototype
是一个普通属性,其值是一个对象。属性 prototype
仅在 new
运算符使用其值作为其创建的实例的原型时才特殊。
**首先,伪方法 constructor
。** 此方法很特殊,因为它定义了表示类的函数
它有时被称为 类构造函数
。它具有一些普通构造函数所没有的特性(主要是能够通过 super()
构造函数调用其超类构造函数,这将在后面解释)。
**其次,静态方法。** *静态属性*(或*类属性*)是 Foo
本身的属性。如果您在方法定义前加上 static
前缀,则会创建一个类方法
**第三,原型方法。** Foo
的*原型属性*是 Foo.prototype
的属性。它们通常是方法,由 Foo
的实例继承。
15.2.2.2 静态数据属性
为了及时完成 ES6 类,它们被故意设计为“尽可能最小化”。这就是为什么您目前只能创建静态方法、getter 和 setter,而不能创建静态数据属性。有一个提案建议将它们添加到语言中。在该提案被接受之前,您可以使用两种解决方法。
首先,您可以手动添加静态属性
您可以使用 Object.defineProperty()
创建只读属性,但我喜欢赋值的简单性。
其次,您可以创建一个静态 getter
在这两种情况下,您都会获得一个可以读取的属性 Point.ZERO
。在第一种情况下,每次都返回相同的实例。在第二种情况下,每次都返回一个新实例。
15.2.2.3 Getter 和 setter
getter 和 setter 的语法与ECMAScript 5 对象字面量中的语法相同
您可以按如下方式使用 MyClass
。
15.2.2.4 计算属性名
如果将方法名放在方括号中,则可以通过表达式定义方法名。例如,以下定义 Foo
的方法是等效的。
ECMAScript 6 中的几种特殊方法的键是符号。计算属性名允许您定义此类方法。例如,如果一个对象有一个键为 Symbol.iterator
的方法,则它是*可迭代的*。这意味着可以使用 for-of
循环和其他语言机制迭代其内容。
15.2.2.5 生成器方法
如果在方法定义前加上星号 (*
),它就会变成一个*生成器方法*。除其他外,生成器可用于定义键为 Symbol.iterator
的方法。以下代码演示了它是如何工作的。
15.2.3 子类化
extends
子句允许您创建现有构造函数的子类(该构造函数可能已通过类定义,也可能未通过类定义)
同样,此类的使用方式与您预期的一样
有两种类
-
Point
是一个*基类*,因为它没有 extends
子句。
-
ColorPoint
是一个*派生类*。
有两种使用 super
的方法
- *类构造函数*(类定义中的伪方法
constructor
)像函数调用一样使用它 (super(···)
),以便进行超类构造函数调用(第 A 行)。
- 方法定义(在对象字面量或类中,无论是否使用
static
)像属性引用 (super.prop
) 或方法调用 (super.method(···)
) 一样使用它,以便引用超类属性(第 B 行)。
15.2.3.1 子类的原型是超类
在 ECMAScript 6 中,子类的原型是超类
这意味着静态属性会被继承
您甚至可以超级调用静态方法
15.2.3.2 超类构造函数调用
在派生类中,您必须先调用 super()
,然后才能使用 this
隐式地使派生构造函数在不调用 super()
的情况下也会导致错误
15.2.3.3 覆盖构造函数的结果
就像在 ES5 中一样,您可以通过显式返回一个对象来覆盖构造函数的结果
如果这样做,则 this
是否已初始化无关紧要。换句话说:如果您以这种方式覆盖结果,则不必在派生构造函数中调用 super()
。
15.2.3.4 类的默认构造函数
如果您没有为基类指定 constructor
,则使用以下定义
对于派生类,使用以下默认构造函数
15.2.3.5 对内置构造函数进行子类化
在 ECMAScript 6 中,您最终可以对所有内置构造函数进行子类化(ES5 的解决方法,但这些方法有很大的局限性)。
例如,您现在可以创建自己的异常类(在大多数引擎中,这些类将继承具有堆栈跟踪的特性)
您还可以创建 Array
的子类,其实例可以正确处理 length
请注意,对 Array
进行子类化通常不是最佳解决方案。通常最好创建自己的类(您可以控制其接口),并将委托给私有属性中的数组。
15.3 类的私有数据
本节介绍四种为 ES6 类管理私有数据的方法
- 将私有数据保存在类
constructor
的环境中
- 通过命名约定(例如,带前缀的下划线)标记私有属性
- 将私有数据保存在 WeakMap 中
- 使用符号作为私有属性的键
方法 #1 和 #2 在 ES5 中已经很常见,用于构造函数。方法 #3 和 #4 是 ES6 中的新增方法。让我们通过每种方法四次实现相同的示例。
15.3.1 通过构造函数环境实现私有数据
我们正在运行的示例是一个名为 Countdown
的类,它在计数器(其初始值为 counter
)达到零时调用回调函数 action
。这两个参数 action
和 counter
应存储为私有数据。
在第一个实现中,我们将 action
和 counter
存储在类构造函数的*环境*中。环境是内部数据结构,JavaScript 引擎在其中存储每次进入新作用域(例如,通过函数调用或构造函数调用)时出现的参数和局部变量。代码如下:
使用 Countdown
的方式如下:
优点
- 私有数据完全安全
- 私有属性的名称不会与其他私有属性(超类或子类)的名称冲突。
缺点
- 代码变得不那么优雅,因为您需要在构造函数内部将所有方法添加到实例中(至少是需要访问私有数据的方法)。
- 由于实例方法,代码会浪费内存。如果这些方法是原型方法,则它们将被共享。
有关此技术的更多信息:请参阅“Speaking JavaScript”中的“构造函数环境中的私有数据(Crockford 隐私模式)”一节。
15.3.2 通过命名约定实现私有数据
以下代码将私有数据保存在名称通过带前缀的下划线标记的属性中
优点
缺点
- 不安全,仅作为客户端代码的准则。
- 私有属性的名称可能会冲突。
15.3.3 通过 WeakMap 实现私有数据
有一种使用 WeakMap 的巧妙技术,它结合了第一种方法(安全性)和第二种方法(能够使用原型方法)的优点。以下代码演示了此技术:我们使用 WeakMap _counter
和 _action
来存储私有数据。
两个 WeakMap _counter
和 _action
中的每一个都将对象映射到其私有数据。由于 WeakMap 的工作方式,这不会阻止对象被垃圾回收。只要您对外部世界隐藏 WeakMap,私有数据就是安全的。
如果您想更加安全,可以将 WeakMap.prototype.get
和 WeakMap.prototype.set
存储在变量中并调用它们(而不是动态调用方法)
然后,如果恶意代码用窥探我们私有数据的方法替换这些方法,您的代码将不会受到影响。但是,您只能防止在您的代码之后运行的代码。如果它在您的代码之前运行,您将无能为力。
优点
- 我们可以使用原型方法。
- 比属性键的命名约定更安全。
- 私有属性的名称不能冲突。
- 相对优雅。
缺点
15.3.4 通过符号实现私有数据
私有数据的另一个存储位置是键为符号的属性
每个符号都是唯一的,这就是为什么符号值属性键永远不会与任何其他属性键冲突的原因。此外,符号在某种程度上对外部世界是隐藏的,但并非完全隐藏
优点
缺点
- 代码不如命名约定优雅。
- 不安全:您可以通过
Reflect.ownKeys()
列出对象的 all 属性键(包括符号!)。
15.3.5 扩展阅读
- “Speaking JavaScript”中的“保持数据私有”一节(涵盖 ES5 技术)
15.4 简单的混入
在 JavaScript 中使用子类有两个原因
- 接口继承:每个是子类实例的对象(通过
instanceof
测试)也是超类的实例。期望是子类实例的行为类似于超类实例,但可能会做更多的事情。
- 实现继承:超类将其功能传递给其子类。
类对于实现继承的用处有限,因为它们只支持单继承(一个类最多只能有一个超类)。因此,不可能从多个来源继承工具方法——它们必须全部来自超类。
那么我们如何解决这个问题呢?让我们通过一个例子来探讨解决方案。考虑一个企业的管理系统,其中 Employee
是 Person
的子类。
此外,还有用于存储和数据验证的工具类
如果我们可以像这样包含工具类就好了
也就是说,我们希望 Employee
是 Storage
的子类,Storage
应该是 Validation
的子类,而 Validation
应该是 Person
的子类。Employee
和 Person
只会在这样一个类链中使用。但是 Storage
和 Validation
将被多次使用。我们希望它们成为我们填充其超类的类的模板。此类模板称为*抽象子类*或*混入*。
在 ES6 中实现混入的一种方法是将其视为一个函数,其输入是一个超类,输出是一个扩展该超类的子类
在这里,我们受益于 extends
子句的操作数不是固定的标识符,而是一个任意表达式。使用这些混入,Employee
的创建方式如下
**致谢。** 我知道的这种技术的第一次出现是 Sebastian Markbåge 的 Gist。
15.5 类的细节
到目前为止,我们所看到的是类的基本要素。只有当您对事物如何在幕后发生感兴趣时,才需要继续阅读。让我们从类的语法开始。以下是 ECMAScript 6 规范的 A.4 节 中显示的语法的略微修改版本。
两点观察
- 要扩展的值可以通过任意表达式生成。这意味着您将能够编写如下代码
- 方法之间允许使用分号。
15.5.1 各种检查
- 错误检查:类名不能是
eval
或 arguments
;不允许重复的类元素名称;名称 constructor
只能用于普通方法,不能用于 getter、setter 或生成器方法。
- 类不能被函数调用。如果它们被函数调用,则会抛出
TypeException
。
- 原型方法不能用作构造函数
15.5.2 属性的特性
类声明创建(可变的)let 绑定。下表描述了与给定类 Foo
相关的属性的特性
|
可写 |
可枚举 |
可配置 |
静态属性 Foo.* |
true |
false |
true |
Foo.prototype |
false |
false |
false |
Foo.prototype.constructor |
false |
false |
true |
原型属性 Foo.prototype.* |
true |
false |
true |
备注
- 许多属性都是可写的,以允许动态修补。
- 构造函数及其属性原型中的对象具有不可变的双向链接。
- 对象字面量中的方法定义会生成可枚举的属性。
15.5.3 类具有内部名称
类具有词法内部名称,就像命名函数表达式一样。
15.5.3.1 命名函数表达式的内部名称
您可能知道命名函数表达式具有词法内部名称
命名函数表达式的名称 me
变成了一个词法绑定变量,不受当前哪个变量持有该函数的影响。
15.5.3.2 类的内部名称
有趣的是,ES6 类也具有词法内部名称,您可以在方法(构造函数方法和常规方法)中使用这些名称
(在 ES6 规范中,内部名称由 ClassDefinitionEvaluation 的动态语义 设置。)
**致谢:** 感谢 Michael Ficarra 指出类具有内部名称。
15.6 子类的细节
在 ECMAScript 6 中,子类如下所示。
下一节将检查由上一个示例创建的对象的结构。之后的一节将检查如何分配和初始化 jane
。
15.6.1 原型链
上一个示例创建了以下对象。
*原型链*是通过 [[Prototype]]
关系(这是一种继承关系)链接的对象。在图中,您可以看到两条原型链
15.6.1.1 左栏:类(函数)
派生类的原型是它扩展的类。这种设置的原因是您希望子类继承其超类的所有属性
基类的原型是 Function.prototype
,它也是函数的原型
这意味着基类及其所有派生类(它们的原型)都是函数。传统的 ES5 函数本质上是基类。
15.6.1.2 右栏:实例的原型链
类的主要目的是建立这条原型链。原型链以 Object.prototype
结束(其原型是 null
)。这使得 Object
成为每个基类的隐式超类(就实例和 instanceof
运算符而言)。
这种设置的原因是您希望子类的实例原型继承超类实例原型的所有属性。
顺便说一句,通过对象字面量创建的对象也具有原型 Object.prototype
15.6.2 分配和初始化实例
类构造函数之间的数据流不同于 ES5 中规范的子类化方式。在幕后,它大致如下所示。
实例对象在 ES6 和 ES5 中的不同位置创建
- 在 ES6 中,它是在基类构造函数中创建的,它是构造函数调用链中的最后一个。超类构造函数通过
super()
调用,这会触发构造函数调用。
- 在 ES5 中,它是在
new
的操作数中创建的,它是构造函数调用链中的第一个。超类构造函数通过函数调用调用。
前面的代码使用了两个新的 ES6 特性
-
new.target
是所有函数都具有的隐式参数。在构造函数调用链中,它的作用类似于超方法调用链中的 this
。
- 如果构造函数是直接通过
new
调用的(如行 B 所示),则 new.target
的值就是该构造函数。
- 如果构造函数是通过
super()
调用的(如行 A 所示),则 new.target
的值是进行调用的构造函数的 new.target
。
- 在正常的函数调用中,它是
undefined
。这意味着您可以使用 new.target
来确定函数是函数调用还是构造函数调用(通过 new
)。
- 在箭头函数内部,
new.target
指的是外部非箭头函数的 new.target
。
-
Reflect.construct()
允许您通过最后一个参数指定 new.target
来进行构造函数调用。
这种子类化方式的优点是,它使普通代码能够对内置构造函数(例如 Error
和 Array
)进行子类化。后面的章节将解释为什么需要不同的方法。
提醒一下,以下是您在 ES5 中进行子类化的方式
15.6.2.1 安全检查
-
this
最初在派生构造函数中未初始化,这意味着如果它们在调用 super()
之前以任何方式访问 this
,则会引发错误。
- 一旦
this
被初始化,调用 super()
将产生 ReferenceError
。这可以防止您两次调用 super()
。
- 如果构造函数隐式返回(没有
return
语句),则结果为 this
。如果 this
未初始化,则会引发 ReferenceError
。这可以防止您忘记调用 super()
。
- 如果构造函数显式返回非对象(包括
undefined
和 null
),则结果为 this
(此行为需要保持与 ES5 及更早版本兼容)。如果 this
未初始化,则会引发 TypeError
。
- 如果构造函数显式返回一个对象,则将其用作结果。然后,
this
是否初始化就无关紧要了。
15.6.2.2 extends
子句
让我们研究一下 extends
子句如何影响类的设置(规范的第 14.5.14 节)。
extends
子句的值必须是“可构造的”(可通过 new
调用)。不过,允许使用 null
。
- 构造函数类型:基类
C
的原型:Function.prototype
(类似于普通函数)
C.prototype
的原型:Object.prototype
(这也是通过对象字面量创建的对象的原型)
- 构造函数类型:派生类
C
的原型:B
C.prototype
的原型:B.prototype
- 构造函数类型:派生类
C
的原型:Object
C.prototype
的原型:Object.prototype
请注意与第一种情况的以下细微差别:如果没有 extends
子句,则该类是基类并分配实例。如果一个类扩展了 Object
,则它是一个派生类,Object
分配实例。生成的实例(包括它们的原型链)是相同的,但您获得它们的方式不同。
- 构造函数类型:基类(从 ES2016 开始)
C
的原型:Function.prototype
C.prototype
的原型:null
这样的类可以让您避免在原型链中使用 Object.prototype
。
15.6.3 为什么不能在 ES5 中对内置构造函数进行子类化?
在 ECMAScript 5 中,大多数内置构造函数都不能进行子类化(存在几种解决方法)。
为了理解原因,让我们使用规范的 ES5 模式对 Array
进行子类化。我们很快就会发现,这不起作用。
不幸的是,如果我们实例化 MyArray
,我们会发现它无法正常工作:实例属性 length
不会随着我们添加数组元素而改变
有两个障碍阻止 myArr
成为一个正确的数组。
**第一个障碍:初始化。** 您传递给构造函数 Array
的 this
(在 A 行)被完全忽略。这意味着您不能使用 Array
来设置为 MyArray
创建的实例。
**第二个障碍:分配。** Array
创建的实例对象是*奇异的*(ECMAScript 规范中用于描述具有普通对象不具备的功能的对象的术语):它们的属性 length
跟踪并影响数组元素的管理。通常,可以从头开始创建奇异对象,但不能将现有的普通对象转换为奇异对象。不幸的是,这就是 Array
在 A 行中调用时必须做的事情:它必须将为 MyArray
创建的普通对象转换为奇异数组对象。
15.6.3.1 解决方案:ES6 子类化
在 ECMAScript 6 中,对 Array
进行子类化如下所示
这有效
让我们研究一下 ES6 的子类化方法是如何消除前面提到的障碍的
- 第一个障碍,即
Array
无法设置实例,通过 Array
返回一个完全配置的实例来消除。与 ES5 不同,此实例具有子类的原型。
- 第二个障碍,即子构造函数不创建奇异实例,通过派生类依赖基类来分配实例来消除。
15.6.4 在方法中引用超属性
以下 ES6 代码在 B 行进行超方法调用。
为了理解超调用的工作原理,让我们看一下 jane
的对象图
在 B 行中,Employee.prototype.toString
对其重写的方法(从 A 行开始)进行超调用(B 行)。让我们将存储方法的对象称为该方法的*宿主对象*。例如,Employee.prototype
是 Employee.prototype.toString()
的宿主对象。
B 行中的超调用涉及三个步骤
- 从当前方法的宿主对象的原型开始搜索。
- 查找名称为
toString
的方法。该方法可以在搜索开始的对象中找到,也可以在原型链的后面找到。
- 使用当前的
this
调用该方法。这样做的原因是:被超调用的方法必须能够访问相同的实例属性(在我们的示例中,是 jane
的自有属性)。
请注意,即使您只是在获取 (super.prop
) 或设置 (super.prop = 123
) 超属性(而不是进行方法调用),this
仍然可能(在内部)在步骤 #3 中发挥作用,因为可能会调用 getter 或 setter。
让我们以三种不同但等效的方式来表达这些步骤
变体 3 是 ECMAScript 6 处理超调用的方式。这种方法得到了函数的*环境*具有的两个内部*绑定*的支持(*环境*为作用域中的变量提供存储空间,即所谓的*绑定*)
-
[[thisValue]]
:此内部绑定在 ECMAScript 5 中也存在,用于存储 this
的值。
-
[[HomeObject]]
:指的是环境函数的宿主对象。通过所有使用 super
的方法都具有的内部插槽 [[HomeObject]]
填充。绑定和插槽都是 ECMAScript 6 中的新增功能。
15.6.4.1 在哪里可以使用 super
?
每当涉及到原型链时,引用超属性都很方便,这就是为什么您可以在对象字面量和类定义内部的方法定义(包括生成器方法定义、getter 和 setter)中使用它的原因。该类可以是派生的,也可以不是派生的,该方法可以是静态的,也可以不是静态的。
在函数声明、函数表达式和生成器函数中,不允许使用 super
来引用属性。
15.6.4.2 陷阱:使用 super
的方法不能移动
您不能移动使用 super
的方法:此类方法具有内部插槽 [[HomeObject]]
,将其绑定到创建它的对象。如果通过赋值移动它,它将继续引用原始对象的超属性。在未来的 ECMAScript 版本中,可能也会有办法转移此类方法。
15.7 物种模式
ECMAScript 6 中内置构造函数的另一个机制已经可以扩展:有时方法会创建其类的新实例。如果您创建一个子类,该方法应该返回其类的实例还是子类的实例?一些内置的 ES6 方法允许您通过所谓的*物种模式*来配置它们如何创建实例。
例如,考虑 Array
的子类 SortedArray
。如果我们在该类的实例上调用 map()
,我们希望它返回 Array
的实例,以避免不必要的排序。默认情况下,map()
返回接收器 (this
) 的实例,但物种模式允许您更改这一点。
15.7.1 示例的辅助方法
在接下来的三节中,我将在示例中使用两个辅助函数
15.7.2 标准物种模式
标准物种模式由 Promise.prototype.then()
、类型化数组的 filter()
方法和其他操作使用。它的工作原理如下
- 如果
this.constructor[Symbol.species]
存在,则将其用作新实例的构造函数。
- 否则,使用默认构造函数(例如,数组的
Array
)。
在 JavaScript 中实现,该模式如下所示
15.7.3 数组的物种模式
普通数组以略微不同的方式实现物种模式
Array.prototype.map()
通过 ArraySpeciesCreate(this, this.length)
创建它返回的数组。
15.7.4 静态方法中的物种模式
Promise 对静态方法(例如 Promise.all()
)使用物种模式的变体
15.7.5 在子类中覆盖默认物种
这是属性 [Symbol.species]
的默认 getter
此默认 getter 由内置类 Array
、ArrayBuffer
、Map
、Promise
、RegExp
、Set
和 %TypedArray%
实现。它由这些内置类的子类自动继承。
您可以通过两种方式覆盖默认物种:使用您选择的构造函数或使用 null
。
15.7.5.1 将物种设置为选择的构造函数
您可以通过静态 getter(A 行)覆盖默认物种
结果,map()
返回 Array
的实例
如果您没有覆盖默认物种,map()
将返回子类的实例
15.7.5.1.1 通过数据属性指定物种
如果您不想使用静态 getter,则需要使用 Object.defineProperty()
。您不能使用赋值,因为已经有一个具有该键的属性,该属性只有一个 getter。这意味着它是只读的,不能被赋值。
例如,这里我们将 MyArray1
的种类设置为 Array
15.7.5.2 将种类设置为 null
如果将种类设置为 null
,则使用默认构造函数(使用哪个构造函数取决于使用的是哪个种类的模式变体,有关详细信息,请参阅前面的章节)。
15.8 类的优缺点
类在 JavaScript 社区中存在争议:一方面,来自基于类的语言的人们很高兴他们不再需要处理 JavaScript 非常规的继承机制。另一方面,许多 JavaScript 程序员认为,JavaScript 复杂的地方不在于原型继承,而在于构造函数。
ES6 类提供了一些明显的优势
- 它们向后兼容许多现有代码。
- 与构造函数和构造函数继承相比,类使初学者更容易上手。
- 语言本身支持子类化。
- 内置构造函数是可子类化的。
- 不再需要继承库;代码在框架之间将更具可移植性。
- 它们为将来的高级功能奠定了基础:特征(或混合)、不可变实例等。
- 它们有助于静态分析代码的工具(IDE、类型检查器、样式检查器等)。
让我们来看看关于 ES6 类的一些常见抱怨。你会发现我同意其中大部分观点,但我认为类的优点远远超过其缺点。我很高兴它们出现在 ES6 中,我建议使用它们。
15.8.1 抱怨:ES6 类掩盖了 JavaScript 继承的本质
是的,ES6 类确实掩盖了 JavaScript 继承的本质。类的外观(语法)与其行为方式(语义)之间存在不幸的脱节:它看起来像一个对象,但它是一个函数。我更希望类是*构造函数对象*,而不是构造函数。我在 Proto.js
项目 中通过一个小型库探索了这种方法(这证明了这种方法的适用性)。
但是,向后兼容性很重要,这就是为什么类作为构造函数也有意义的原因。这样,ES6 代码和 ES5 代码更具互操作性。
语法和语义之间的脱节会在 ES6 及更高版本中造成一些摩擦。但是,您可以通过简单地从表面上理解 ES6 类来过上舒适的生活。我认为这种错觉永远不会困扰你。新手可以更快地入门,并在以后(在他们对该语言更加熟悉之后)阅读幕后发生的事情。
15.8.2 抱怨:类只提供单继承
类只提供单继承,这严重限制了您在面向对象设计方面的表达自由。但是,计划一直是让它们成为多重继承机制(例如特征)的基础。
然后,类成为一个可实例化的实体和一个组装特征的位置。在此之前,如果您想要多重继承,则需要求助于库。
15.8.3 抱怨:由于强制使用 new
,类将您锁定
如果要实例化一个类,在 ES6 中必须使用 new
。这意味着您无法在不更改调用站点的情况下从类切换到工厂函数。这确实是一个限制,但有两个缓解因素
- 您可以通过从类的
constructor
方法返回一个对象来覆盖 new
运算符返回的默认结果。
- 由于其内置的模块和类,ES6 使 IDE 更容易重构代码。因此,从
new
到函数调用将变得很简单。显然,如果您无法控制调用您代码的代码(如库的情况),这对您没有帮助。
因此,类在语法上*确实*限制了您,但是,一旦 JavaScript 具有特征,它们就不会在*概念上*(关于面向对象设计)限制您。
15.9 常见问题解答:类
15.9.1 为什么不能函数调用类?
目前禁止函数调用类。这样做是为了为将来保留选项,以便最终添加一种通过类处理函数调用的方法。
15.9.2 给定一个参数数组,如何实例化一个类?
类的 Function.prototype.apply()
的类似物是什么?也就是说,如果我有一个类 TheClass
和一个参数数组 args
,我该如何实例化 TheClass
?
一种方法是通过扩展运算符 (...
)
另一种选择是使用 Reflect.construct()
15.10 类的下一步是什么?
类的设计格言是“最大限度地最小化”。讨论了几个高级功能,但最终为了获得 TC39 一致接受的设计而放弃了这些功能。
ECMAScript 的未来版本现在可以扩展这种最小化设计——类将为特征(或混合)、值对象(如果具有相同内容,则不同对象相等)和常量类(产生不可变实例)等功能奠定基础。
15.11 延伸阅读
以下文档是本章的重要来源