extends

Baseline 已广泛支持

此特性已经非常成熟,并且适用于许多设备和浏览器版本。自 2016 年 3 月以来,它已在所有浏览器中可用。

extends 关键字用于 类声明类表达式 中,用来创建一个作为另一个类的子类的类。

试一试

class DateFormatter 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()}`;
  }
}

console.log(new DateFormatter("August 19, 1975 23:15:30").getFormattedDate());
// Expected output: "19-Aug-1975"

语法

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

一个求值为构造函数(包括类)或 null 的表达式。

描述

extends 关键字可用于子类化自定义类以及内置对象。

任何可以使用 new 调用并具有 prototype 属性的构造函数都可以作为父类。这两个条件必须同时满足——例如,绑定函数Proxy 可以构造,但它们没有 prototype 属性,因此不能子类化。

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 操作符忽略。)

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 都设置了原型。

ChildClass 的原型 ChildClass.prototype 的原型
缺少 extends 子句 Function.prototype Object.prototype
extends null Function.prototype null
extends 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 在此表达式中按预期工作。

js
class SomeClass extends class {
  constructor() {
    console.log("Base class");
  }
} {
  constructor() {
    super();
    console.log("Derived class");
  }
}

new SomeClass();
// Base class
// Derived class

虽然基类可以从其构造函数返回任何东西,但派生类必须返回一个对象或 undefined,否则会抛出 TypeError

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 值。这个技巧被称为 “返回覆盖”,它允许派生类的字段(包括 私有 字段)定义在不相关的对象上。

子类化内置对象

警告: 标准委员会现在认为以前规范版本中的内置子类化机制设计过度,并导致不可忽略的性能和安全影响。新的内置方法较少考虑子类,并且引擎实现者正在 调查是否移除某些子类化机制。在增强内置对象时,请考虑使用组合而不是继承。

以下是扩展类时可能遇到的一些情况:

  • 当在子类上调用静态工厂方法(如 Promise.resolve()Array.from())时,返回的实例始终是子类的实例。
  • 当在子类上调用返回新实例的实例方法(如 Promise.prototype.then()Array.prototype.map())时,返回的实例始终是子类的实例。
  • 实例方法在可能的情况下会尝试委托给最少的基本方法集。例如,对于 Promise 的子类,覆盖 then() 会自动导致 catch() 的行为改变;或者对于 Map 的子类,覆盖 set() 会自动导致 Map() 构造函数的行为改变。

然而,上述期望的实现需要付出不小的努力。

  • 第一个要求静态方法读取 this 的值以获取用于构造返回实例的构造函数。这意味着 [p1, p2, p3].map(Promise.resolve) 会抛出错误,因为 Promise.resolve 内部的 thisundefined。解决此问题的一种方法是,如果 this 不是构造函数,则回退到基类,就像 Array.from() 所做的那样,但这仍然意味着基类是特殊处理的。
  • 第二个要求实例方法读取 this.constructor 以获取构造函数。然而,new this.constructor() 可能会破坏旧代码,因为 constructor 属性是可写和可配置的,并且没有受到任何保护。因此,许多复制内置方法转而使用构造函数的 [Symbol.species] 属性(默认情况下它只返回 this,即构造函数本身)。然而,[Symbol.species] 允许运行任意代码并创建任意类型的实例,这带来了安全隐患并极大地复杂化了子类化语义。
  • 第三个导致自定义代码的可见调用,这使得许多优化更难实现。例如,如果使用 x 个元素的迭代器调用 Map() 构造函数,那么它必须显式调用 set() 方法 x 次,而不是仅仅将元素复制到内部存储中。

这些问题并非内置类独有。对于你自己的类,你可能需要做出相同的决定。然而,对于内置类,可优化性和安全性是一个更大的问题。新的内置方法总是构造基类并尽可能少地调用自定义方法。如果你想在实现上述期望的同时子类化内置对象,你需要覆盖所有默认行为已嵌入其中的方法。基类上任何新方法的添加也可能破坏你子类的语义,因为它们默认会被继承。因此,扩展内置对象的更好方法是使用 组合

继承 null

extends null 的设计是为了方便创建 不继承自 Object.prototype 的对象。然而,由于关于是否应该在构造函数中调用 super() 的决策尚未确定,因此在实践中无法使用任何不返回对象的构造函数实现来构造此类。TC39 委员会正在努力重新启用此功能

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

相反,你需要显式地从构造函数返回一个实例。

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

示例

使用 extends

第一个例子是从一个名为 Polygon 的类创建一个名为 Square 的类。这个例子摘自这个 实时演示 (来源)

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;
  }
}

扩展普通对象

类不能扩展常规(不可构造的)对象。如果你想通过使该对象的所有属性在继承实例上可用,从而从常规对象继承,你可以使用 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.

扩展内置对象

此示例扩展了内置的 Date 对象。此示例摘自这个 实时演示 (来源)

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

所有 JavaScript 对象默认都继承自 Object.prototype,所以乍一看 extends Object 似乎是多余的。与完全不写 extends 的唯一区别是,构造函数本身继承了来自 Object 的静态方法,例如 Object.keys()。然而,由于没有 Object 静态方法使用 this 值,继承这些静态方法仍然没有价值。

在子类化场景中,Object() 构造函数会进行特殊处理。如果它通过 super() 隐式调用,它总是使用 new.target.prototype 作为其原型来初始化一个新对象。传递给 super() 的任何值都会被忽略。

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' ]

将此行为与不特殊处理子类化的自定义包装器进行比较

js
function MyObject(v) {
  return new Object(v);
}
class D extends MyObject {
  constructor(v) {
    super(v);
  }
}
console.log(new D(1) instanceof Number); // true

种类

你可能希望在派生数组类 MyArray 中返回 Array 对象。种类模式允许你覆盖默认构造函数。

例如,当使用 Array.prototype.map() 等返回默认构造函数的方法时,你希望这些方法返回父 Array 对象,而不是 MyArray 对象。Symbol.species 符号允许你这样做

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

这种行为由许多内置复制方法实现。有关此功能的注意事项,请参阅子类化内置对象讨论。

混入(Mix-ins)

抽象子类或混入是类的模板。一个类只能有一个超类,因此不可能实现工具类等的多重继承。功能必须由超类提供。

一个以超类为输入并以扩展该超类的子类为输出的函数可用于实现混入

js
const calculatorMixin = (Base) =>
  class extends Base {
    calc() {}
  };

const randomizerMixin = (Base) =>
  class extends Base {
    randomize() {}
  };

然后,使用这些混入的类可以这样编写

js
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}

避免继承

继承是面向对象编程中非常强的耦合关系。这意味着基类的所有行为默认都被子类继承,这可能并非总是你想要的。例如,考虑 ReadOnlyMap 的实现

js
class ReadOnlyMap extends Map {
  set() {
    throw new TypeError("A read-only map must be set at construction time.");
  }
}

结果是 ReadOnlyMap 不可构造,因为 Map() 构造函数会调用实例的 set() 方法。

js
const m = new ReadOnlyMap([["a", 1]]); // TypeError: A read-only map must be set at construction time.

我们可以通过使用一个私有标志来指示实例是否正在被构造来解决这个问题。然而,这种设计的一个更重要的问题是它违反了 里氏替换原则,该原则指出子类应该能够替换其超类。如果一个函数期望一个 Map 对象,它也应该能够使用一个 ReadOnlyMap 对象,这在这里会被破坏。

继承常常导致 圆-椭圆问题,因为尽管它们共享许多共同特征,但两者都不能完美地包含对方的行为。通常,除非有非常充分的理由使用继承,否则最好使用组合。组合意味着一个类引用另一个类的对象,并且只将该对象用作实现细节。

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() 的新实用方法(例如 getOrInsert()),则除非 ReadOnlyMap 类也相应更新以覆盖 getOrInsert(),否则它将不再是只读的。此外,ReadOnlyMap 对象根本没有 set 方法,这比在运行时抛出错误更准确。

规范

规范
ECMAScript® 2026 语言规范
# sec-class-definitions

浏览器兼容性

另见