extends
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since March 2017.
extends
关键字在 类声明 或 类表达式 中使用来创建一个类,该类是另一个类的子类。
¥The extends
keyword is used in class declarations or class expressions to create a class that is a child of another class.
Try it
语法
描述
¥Description
extends
关键字可用于子类化自定义类以及内置对象。
¥The extends
keyword can be used to subclass custom classes as well as built-in objects.
任何可以使用 new
调用并具有 prototype
属性的构造函数都可以作为父类的候选构造函数。这两个条件必须同时成立 - 例如,可以构造 绑定函数 和 Proxy
,但它们没有 prototype
属性,因此不能对它们进行子类化。
¥Any constructor that can be called with new
and has the prototype
property can be the candidate for the parent class. The two conditions must both hold — for example, bound functions and Proxy
can be constructed, but they don't have a prototype
property, so they cannot be subclassed.
function OldStyleClass() {
this.someProperty = 1;
}
OldStyleClass.prototype.someMethod = function () {};
class ChildClass extends OldStyleClass {}
class ModernClass {
someProperty = 1;
someMethod() {}
}
class AnotherChildClass extends ModernClass {}
ParentClass
的 prototype
属性必须是 Object
或 null
,但在实践中你很少会担心这一点,因为非对象 prototype
无论如何都不会表现出应有的行为。(它被 new
运算符忽略。)
¥The prototype
property of the ParentClass
must be an Object
or null
, but you would rarely worry about this in practice, because a non-object prototype
doesn't behave as it should anyway. (It's ignored by the new
operator.)
function ParentClass() {}
ParentClass.prototype = 3;
class ChildClass extends ParentClass {}
// Uncaught TypeError: Class extends value does not have valid prototype property 3
console.log(Object.getPrototypeOf(new ParentClass()));
// [Object: null prototype] {}
// Not actually a number!
extends
为 ChildClass
和 ChildClass.prototype
设定了原型。
¥extends
sets the prototype for both ChildClass
and ChildClass.prototype
.
ChildClass 原型机 |
ChildClass.prototype 原型机 |
|
---|---|---|
extends 条款缺失 |
Function.prototype |
Object.prototype |
extends null |
Function.prototype |
null |
extends ParentClass |
ParentClass |
ParentClass.prototype |
class ParentClass {}
class ChildClass extends ParentClass {}
// Allows inheritance of static properties
Object.getPrototypeOf(ChildClass) === ParentClass;
// Allows inheritance of instance properties
Object.getPrototypeOf(ChildClass.prototype) === ParentClass.prototype;
extends
的右侧不必是标识符。你可以使用任何计算结果为构造函数的表达式。这对于创建 mixins 通常很有用。extends
表达式中的 this
值是围绕类定义的 this
,并且引用类名称的是 ReferenceError
,因为该类尚未初始化。await
和 yield
在此表达式中按预期工作。
¥The right-hand side of extends
does not have to be an identifier. You can use any expression that evaluates to a constructor. This is often useful to create mixins. The this
value in the extends
expression is the this
surrounding the class definition, and referring to the class's name is a ReferenceError
because the class is not initialized yet. await
and yield
work as expected in this expression.
class SomeClass extends class {
constructor() {
console.log("Base class");
}
} {
constructor() {
super();
console.log("Derived class");
}
}
new SomeClass();
// Base class
// Derived class
虽然基类可以从其构造函数返回任何内容,但派生类必须返回一个对象或 undefined
,否则将抛出 TypeError
。
¥While the base class may return anything from its constructor, the derived class must return an object or undefined
, or a TypeError
will be thrown.
class ParentClass {
constructor() {
return 1;
}
}
console.log(new ParentClass()); // ParentClass {}
// The return value is ignored because it's not an object
// This is consistent with function constructors
class ChildClass extends ParentClass {
constructor() {
super();
return 1;
}
}
console.log(new ChildClass()); // TypeError: Derived constructors may only return object or undefined
如果父类构造函数返回一个对象,则在进一步初始化 类字段 时,该对象将用作派生类的 this
值。这个技巧称为 "返回覆盖",它允许派生类的字段(包括 private 字段)在不相关的对象上定义。
¥If the parent class constructor returns an object, that object will be used as the this
value for the derived class when further initializing class fields. This trick is called "return overriding", which allows a derived class's fields (including private ones) to be defined on unrelated objects.
子类化内置函数
¥Subclassing built-ins
警告:标准委员会现在的立场是,以前规范版本中的内置子类化机制是过度设计的,会造成不可忽视的性能和安全影响。新的内置方法较少考虑子类,引擎实现者是 研究是否删除某些子类化机制。增强内置函数时,请考虑使用组合而不是继承。
¥Warning: The standard committee now holds the position that the built-in subclassing mechanism in previous spec versions is over-engineered and causes non-negligible performance and security impacts. New built-in methods consider less about subclasses, and engine implementers are investigating whether to remove certain subclassing mechanisms. Consider using composition instead of inheritance when enhancing built-ins.
以下是扩展类时你可能会遇到的一些情况:
¥Here are some things you may expect when extending a class:
- 当在子类上调用静态工厂方法(如
Promise.resolve()
或Array.from()
)时,返回的实例始终是子类的实例。 - 当调用子类上返回新实例(如
Promise.prototype.then()
或Array.prototype.map()
)的实例方法时,返回的实例始终是子类的实例。 - 实例方法尝试尽可能委托给一组最小的原始方法。例如,对于
Promise
的子类,重写then()
会自动导致catch()
的行为发生改变;或者对于Map
的子类,重写set()
会自动导致Map()
构造函数的行为发生更改。
然而,上述期望的落实需要付出不小的努力。
¥However, the above expectations take non-trivial efforts to implement properly.
- 第一个需要静态方法读取
this
的值来获取构造返回实例的构造函数。这意味着[p1, p2, p3].map(Promise.resolve)
会抛出错误,因为Promise.resolve
中的this
是undefined
。解决此问题的一种方法是,如果this
不是构造函数(如Array.from()
那样),则回退到基类,但这仍然意味着基类是特殊情况。 - 第二个需要实例方法读取
this.constructor
来获取构造函数。但是,new this.constructor()
可能会破坏遗留代码,因为constructor
属性既可写又可配置,并且不受任何方式的保护。因此,许多复制内置方法使用构造函数的[Symbol.species]
属性(默认情况下仅返回this
,即构造函数本身)。然而,[Symbol.species]
允许运行任意代码并创建任意类型的实例,这带来了安全问题并使子类化语义变得非常复杂。 - 第三种导致可见的自定义代码调用,这使得许多优化更难实现。例如,如果使用 x 个可迭代元素调用
Map()
构造函数,则它必须明显调用set()
方法 x 次,而不是仅仅将元素复制到内部存储中。
这些问题并非内置类所独有。对于你自己的课程,你可能必须做出相同的决定。然而,对于内置类,可优化性和安全性是一个更大的问题。新的内置方法始终构造基类并调用尽可能少的自定义方法。如果你想在实现上述期望的同时对内置函数进行子类化,则需要重写所有具有默认行为的方法。在基类上添加任何新方法也可能会破坏子类的语义,因为它们是默认继承的。因此,扩展内置函数的更好方法是使用 composition。
¥These problems are not unique to built-in classes. For your own classes, you will likely have to make the same decisions. However, for built-in classes, optimizability and security are a much bigger concern. New built-in methods always construct the base class and call as few custom methods as possible. If you want to subclass built-ins while achieving the above expectations, you need to override all methods that have the default behavior baked into them. Any addition of new methods on the base class may also break the semantics of your subclass because they are inherited by default. Therefore, a better way to extend built-ins is to use composition.
扩展空值
¥Extending null
extends null
旨在允许轻松创建 不继承自 Object.prototype
的对象。然而,由于关于是否应该在构造函数中调用 super()
的决定尚未确定,因此在实践中不可能使用任何不返回对象的构造函数实现来构造这样的类。TC39 委员会正在努力重新启用此功能。
¥extends null
was designed to allow easy creation of objects that do not inherit from Object.prototype
. However, due to unsettled decisions about whether super()
should be called within the constructor, it's not possible to construct such a class in practice using any constructor implementation that doesn't return an object. The TC39 committee is working on re-enabling this feature.
new (class extends null {})();
// TypeError: Super constructor null of anonymous class is not a constructor
new (class extends null {
constructor() {}
})();
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
new (class extends null {
constructor() {
super();
}
})();
// TypeError: Super constructor null of anonymous class is not a constructor
相反,你需要从构造函数显式返回一个实例。
¥Instead, you need to explicitly return an instance from the constructor.
class NullClass extends null {
constructor() {
// Using new.target allows derived classes to
// have the correct prototype chain
return Object.create(new.target.prototype);
}
}
const proto = Object.getPrototypeOf;
console.log(proto(proto(new NullClass()))); // null
示例
使用扩展
¥Using extends
第一个示例从名为 Polygon
的类创建一个名为 Square
的类。这个例子是摘自这个 现场演示 (来源)。
¥The first example creates a class called Square
from a class called Polygon
. This example is extracted from this live demo (source).
class Square extends Polygon {
constructor(length) {
// Here, it calls the parent class' constructor with lengths
// provided for the Polygon's width and height
super(length, length);
// Note: In derived classes, super() must be called before you
// can use 'this'. Leaving this out will cause a reference error.
this.name = "Square";
}
get area() {
return this.height * this.width;
}
}
扩展普通对象
¥Extending plain objects
类不能扩展常规(不可构造)对象。如果你想通过使该对象的所有属性在继承实例上可用来从常规对象继承,则可以使用 Object.setPrototypeOf()
:
¥Classes cannot extend regular (non-constructible) objects. If you want to inherit from a regular object by making all properties of this object available on inherited instances, you can instead use Object.setPrototypeOf()
:
const Animal = {
speak() {
console.log(`${this.name} makes a noise.`);
},
};
class Dog {
constructor(name) {
this.name = name;
}
}
Object.setPrototypeOf(Dog.prototype, Animal);
const d = new Dog("Mitzie");
d.speak(); // Mitzie makes a noise.
扩展内置对象
¥Extending built-in objects
此示例扩展了内置 Date
对象。这个例子是摘自这个 现场演示 (来源)。
¥This example extends the built-in Date
object. This example is extracted from this live demo (source).
class MyDate extends Date {
getFormattedDate() {
const months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
return `${this.getDate()}-${months[this.getMonth()]}-${this.getFullYear()}`;
}
}
延长 Object
¥Extending Object
所有的 JavaScript 对象默认都继承自 Object.prototype
,所以写 extends Object
乍一看似乎是多余的。和根本不写 extends
的唯一区别就是构造函数本身继承了 Object
的静态方法,比如 Object.keys()
。但是,由于没有 Object
静态方法使用 this
值,因此继承这些静态方法仍然没有任何价值。
¥All JavaScript objects inherit from Object.prototype
by default, so writing extends Object
at first glance seems redundant. The only difference from not writing extends
at all is that the constructor itself inherits static methods from Object
, such as Object.keys()
. However, because no Object
static method uses the this
value, there's still no value in inheriting these static methods.
Object()
构造函数是子类化场景的特例。如果通过 super()
隐式调用它,它总是初始化一个以 new.target.prototype
作为其原型的新对象。传递给 super()
的任何值都将被忽略。
¥The Object()
constructor special-cases the subclassing scenario. If it's implicitly called via super()
, it always initializes a new object with new.target.prototype
as its prototype. Any value passed to super()
is ignored.
class C extends Object {
constructor(v) {
super(v);
}
}
console.log(new C(1) instanceof Number); // false
console.log(C.keys({ a: 1, b: 2 })); // [ 'a', 'b' ]
将此行为与不进行特殊情况子类化的自定义封装器进行比较:
¥Compare this behavior with a custom wrapper that does not special-case subclassing:
function MyObject(v) {
return new Object(v);
}
class D extends MyObject {
constructor(v) {
super(v);
}
}
console.log(new D(1) instanceof Number); // true
物种
¥Species
你可能希望在派生数组类 MyArray
中返回 Array
对象。物种模式允许你覆盖默认构造函数。
¥You might want to return Array
objects in your derived array class MyArray
. The species pattern lets you override default constructors.
例如,当使用 Array.prototype.map()
等返回默认构造函数的方法时,你希望这些方法返回父 Array
对象,而不是 MyArray
对象。Symbol.species
符号可让你执行此操作:
¥For example, when using methods such as Array.prototype.map()
that return the default constructor, you want these methods to return a parent Array
object, instead of the MyArray
object. The Symbol.species
symbol lets you do this:
class MyArray extends Array {
// Overwrite species to the parent Array constructor
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const mapped = a.map((x) => x * x);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
此行为是通过许多内置复制方法实现的。有关此功能的注意事项,请参阅 子类化内置函数 讨论。
¥This behavior is implemented by many built-in copying methods. For caveats of this feature, see the subclassing built-ins discussion.
混入
¥Mix-ins
抽象子类或混合类是类的模板。一个类只能有一个超类,因此不可能从工具类进行多重继承。该功能必须由超类提供。
¥Abstract subclasses or mix-ins are templates for classes. A class can only have a single superclass, so multiple inheritance from tooling classes, for example, is not possible. The functionality must be provided by the superclass.
以超类作为输入和扩展该超类作为输出的子类的函数可用于实现混合:
¥A function with a superclass as input and a subclass extending that superclass as output can be used to implement mix-ins:
const calculatorMixin = (Base) =>
class extends Base {
calc() {}
};
const randomizerMixin = (Base) =>
class extends Base {
randomize() {}
};
使用这些混合的类可以这样编写:
¥A class that uses these mix-ins can then be written like this:
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}
避免继承
¥Avoiding inheritance
继承是面向对象编程中一种很强的耦合关系。这意味着子类默认继承基类的所有行为,这可能并不总是你想要的。例如,考虑 ReadOnlyMap
的实现:
¥Inheritance is a very strong coupling relationship in object-oriented programming. It means all behaviors of the base class are inherited by the subclass by default, which may not always be what you want. For example, consider the implementation of a ReadOnlyMap
:
class ReadOnlyMap extends Map {
set() {
throw new TypeError("A read-only map must be set at construction time.");
}
}
事实证明,ReadOnlyMap
是不可构造的,因为 Map()
构造函数调用了实例的 set()
方法。
¥It turns out that ReadOnlyMap
is not constructible, because the Map()
constructor calls the instance's set()
method.
const m = new ReadOnlyMap([["a", 1]]); // TypeError: A read-only map must be set at construction time.
我们可以通过使用私有标志来指示实例是否正在构建来解决这个问题。然而,这种设计的一个更重要的问题是它破坏了 里氏替换原则,即子类应该可以替换其超类。如果函数需要 Map
对象,它也应该能够使用 ReadOnlyMap
对象,这将在此处中断。
¥We may get around this by using a private flag to indicate whether the instance is being constructed. However, a more significant problem with this design is that it breaks the Liskov substitution principle, which states that a subclass should be substitutable for its superclass. If a function expects a Map
object, it should be able to use a ReadOnlyMap
object as well, which will break here.
继承通常会导致 圆椭圆问题,因为两种类型都不能完美地体现另一种类型的行为,尽管它们有很多共同特性。一般来说,除非有充分的理由使用继承,否则最好使用组合。组合意味着一个类拥有对另一个类的对象的引用,并且仅使用该对象作为实现细节。
¥Inheritance often leads to the circle-ellipse problem, because neither type perfectly entails the behavior of the other, although they share a lot of common traits. In general, unless there's a very good reason to use inheritance, it's better to use composition instead. Composition means that a class has a reference to an object of another class, and only uses that object as an implementation detail.
class ReadOnlyMap {
#data;
constructor(values) {
this.#data = new Map(values);
}
get(key) {
return this.#data.get(key);
}
has(key) {
return this.#data.has(key);
}
get size() {
return this.#data.size;
}
*keys() {
yield* this.#data.keys();
}
*values() {
yield* this.#data.values();
}
*entries() {
yield* this.#data.entries();
}
*[Symbol.iterator]() {
yield* this.#data[Symbol.iterator]();
}
}
在这种情况下,ReadOnlyMap
类不是 Map
的子类,但它仍然实现了大部分相同的方法。这意味着更多的代码重复,但也意味着 ReadOnlyMap
类与 Map
类不是强耦合的,如果 Map
类改变也不容易破坏,避免了 内置子类化的语义问题。例如,如果 Map
类添加了一个不调用 set()
的 emplace()
方法,则会导致 ReadOnlyMap
类不再是只读的,除非后者相应地更新以覆盖 emplace()
。而且,ReadOnlyMap
对象根本没有 set
方法,这比运行时抛出错误更准确。
¥In this case, the ReadOnlyMap
class is not a subclass of Map
, but it still implements most of the same methods. This means more code duplication, but it also means that the ReadOnlyMap
class is not strongly coupled to the Map
class, and does not easily break if the Map
class is changed, avoiding the semantic issues of built-in subclassing. For example, if the Map
class adds an emplace()
method that does not call set()
, it would cause the ReadOnlyMap
class to no longer be read-only unless the latter is updated accordingly to override emplace()
as well. Moreover, ReadOnlyMap
objects do not have the set
method at all, which is more accurate than throwing an error at runtime.
规范
Specification |
---|
ECMAScript Language Specification # sec-class-definitions |
浏览器兼容性
BCD tables only load in the browser
也可以看看
¥See also
- 使用类 指南
- 类
constructor
class
super