Get to know MDN better
求值为构造函数(包括类)或 null 的表达式。
extends 关键字用来创建自定义类或者内置对象的子类。
任何可以用 new 调用并具有 prototype 属性的构造函数都可以作为候选的父类的构造函数。这两个条件必须同时成立——例如,绑定函数和 Proxy 可以被构造,但它们没有 prototype 属性,因此不能被子类化。
ParentClass 的 prototype 属性必须是 Object 或 null,但在实践中很少需要担心这个问题,因为非对象的 prototype 无论如何都不会按照应有的方式运行(new 运算符会忽略它)。
extends 为 ChildClass 和 ChildClass.prototype 设置了原型。
| 缺少 extends | Function.prototype | Object.prototype |
| extends null | Function.prototype | null |
| extends ParentClass | ParentClass | ParentClass.prototype |
extend 的右侧不一定是标识符。你可以使用任何求值为构造函数的表达式。这通常有助于创建混入(mixin)。extends 表达式中的 this 值是围绕类定义的 this ,而引用类的名称会导致 ReferenceError,因为类尚未初始化。在此表达式中,await 和 yield 按预期工作。
基类可以从构造函数中返回任何内容,而派生类必须返回对象或 undefined ,否则将抛出 TypeError。
如果父类构造函数返回一个对象,则在进一步初始化类字段时,该对象将被用作派生类的 this 值。这种技巧被称为“返回覆盖”,它允许在无关对象上定义派生类的字段(包括私有字段)。
警告:标准委员会目前的立场是,以前版本规范中的内置类的子类化机制设计过度,对性能和安全性造成了不可忽视的影响。新的内置方法较少考虑子类,引擎实现者正在研究是否要删除某些子类机制。在增强内置类时,请考虑使用组合而非继承。
下面是扩展类时可能会遇到的一些问题:
然而,要正确地实现上述期望,需要付出不小的努力。
这些问题并非内置类所独有。对于你自己的类,你也可能需要做出同样的决定。不过,对于内置类来说,可优化性和安全性是更大的问题。新的内置方法总是构造基类,并尽可能少地调用自定义方法。如果你想在实现上述期望的同时对内置类进行子类化,你需要重写所有已具有默认行为的方法。在基类上添加任何新方法都可能会破坏子类的语义,因为这些方法是默认继承的。因此,扩展内置类的更好方法是使用组合。
extends null 设计用于轻松创建不继承自 Object.prototype 的对象。然而,由于关于是否应在构造函数中调用 super() 的决定尚未确定,因此在实践中不可能使用任何不返回对象的构造函数实现来构造这样的类。TC39 委员会正在努力重新启用这一特性。
相反,你需要从构造函数中明确返回一个实例。
第一个例子是根据名为 Polygon 类创建一个名为 Square 的类。当前示例是从这个在线演示中提取出来的(源代码)。
类不能扩展常规(不可构造)对象。如果想通过在继承实例中使用常规对象的所有属性来继承该对象,可以使用 Object.setPrototypeOf() 代替:
这个示例继承了内置的 Date 对象。当前示例是从这个在线演示中提取出来的(源代码)。
所有 JavaScript 对象默认情况下都继承自 Object.prototype,因此乍一看,编写 extends Object 似乎是多余的。与完全不写 extends 的唯一区别是构造函数本身继承了 Object 的静态方法,例如 Object.keys()。然而,由于没有任何 Object 静态方法会使用 this 值,因此继承这些静态方法仍然没有任何价值。
Object() 构造函数特殊处理了子类化情况。如果通过 super() 隐式调用该构造函数,则该构造函数始终以 new.target.prototype 为原型初始化一个新对象。传递给 super() 的任何值都将被忽略。
将这种行为与不对子类进行特殊处理的自定义包装器进行比较:
你可能希望在派生数组类 MyArray 中返回 Array 对象。Species 模式可让你覆盖默认构造函数。
例如,在使用 Array.prototype.map() 等返回默认构造函数的方法时,你希望这些方法返回的是父 Array 对象,而不是 MyArray 对象。Symbol.species 符号可让你做到这一点:
许多内置复制方法都实现了这一行为。有关此功能的注意事项,请参阅子类化内置类讨论。
抽象子类或混入是类的模板。一个类只能有一个父类,因此不可能从工具类等多重继承。功能必须由超类提供。
一个以父类为输入,以扩展该父类的子类为输出的函数可以用来实现混入:
使用这些混入的类可以这样编写:
在面向对象编程中,继承是一种非常强的耦合关系。它意味着子类默认继承基类的所有行为,但这并不总是你想要的。例如,请看 ReadOnlyMap 的实现:
结果发现 ReadOnlyMap 无法构造,因为 Map() 构造函数调用了实例的 set() 方法。
我们可以通过使用一个私有标志来指示是否正在构造实例来解决这个问题。然而,这种设计的一个更重要的问题是,它破坏了里氏替换原则,该原则规定子类应该可以替换其超类。如果函数期望使用一个 Map 对象,那么它也应该能够使用一个 ReadOnlyMap 对象,这在这里就会被打破。
继承常常会导致圆——椭圆问题,因为两种类型虽然有很多共同特征,但都不能完美地包含另一种类型的行为。一般来说,除非有非常充分的理由使用继承,否则最好使用组合。组合是指一个类拥有另一个类对象的引用,但只将该对象用作实现细节。
在这种情况下,ReadOnlyMap 类不是 Map 的子类,但它仍然实现了大部分相同的方法。这意味着更多的代码重复,但也意味着 ReadOnlyMap 类与 Map 类不是强耦合的,并且在 Map 类更改时不会轻易中断,从而避免了子类化内置类的语义问题。例如,如果 Map 类添加了一个不调用 set() 的 emplace() 方法,就会导致 ReadOnlyMap 类不再是只读的,除非后者也相应地更新以覆盖 emplace()。此外,ReadOnlyMap 对象根本没有 set 方法,这比在运行时抛出错误更准确。
| ECMAScript® 2027 Language Specification # sec-class-definitions |
启用 JavaScript 以查看此浏览器兼容性表。