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

语法

¥Syntax

js
class ChildClass extends ParentClass { /* … */ }
ParentClass

计算结果为构造函数(包括类)或 null 的表达式。

描述

¥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.

js
function OldStyleClass() {
  this.someProperty = 1;
}
OldStyleClass.prototype.someMethod = function () {};

class ChildClass extends OldStyleClass {}

class ModernClass {
  someProperty = 1;
  someMethod() {}
}

class AnotherChildClass extends ModernClass {}

ParentClassprototype 属性必须是 Objectnull,但在实践中你很少会担心这一点,因为非对象 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.)

js
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!

extendsChildClassChildClass.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
js
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,因为该类尚未初始化。awaityield 在此表达式中按预期工作。

¥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.

js
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.

js
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 中的 thisundefined。解决此问题的一种方法是,如果 this 不是构造函数(如 Array.from() 那样),则回退到基类,但这仍然意味着基类是特殊情况。
  • 第二个需要实例方法读取 this.constructor 来获取构造函数。但是,new this.constructor() 可能会破坏遗留代码,因为 constructor 属性既可写又可配置,并且不受任何方式的保护。因此,许多复制内置方法使用构造函数的 @@species 属性(默认情况下仅返回 this,即构造函数本身)。然而,@@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.

js
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.

js
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

示例

¥Examples

使用扩展

¥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).

js
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():

js
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).

js
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.

js
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:

js
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:

js
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:

js
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:

js
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:

js
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.

js
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.

js
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

¥Specifications

浏览器兼容性

BCD tables only load in the browser

¥Browser compatibility

也可以看看

¥See also