extends

基线 广泛可用

此功能已很好地建立,并且可以在许多设备和浏览器版本中使用。它自以下时间起在所有浏览器中都可用 2017 年 3 月.

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

试一试

语法

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!

extends 设置了 ChildClassChildClass.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 的右侧不必是标识符。您可以使用任何计算结果为构造函数的表达式。这通常用于创建 mixinextends 表达式中的 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] 允许运行任意代码并创建任意类型的实例,这会带来安全问题,并且极大地增加了子类化的语义复杂性。
  • 第三个会导致自定义代码的可见调用,这使得许多优化难以实现。例如,如果 Map() 构造函数被调用时带有包含 x 个元素的可迭代对象,则它必须可见地调用 xset() 方法,而不是仅仅将元素复制到内部存储中。

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

扩展 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

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

混合

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

可以使用一个函数,该函数以超类作为输入,并以扩展该超类的子类作为输出,来实现混合。

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 类添加了一个 emplace() 方法,该方法不调用 set(),则会导致 ReadOnlyMap 类不再是只读的,除非后者相应地更新以覆盖 emplace()。此外,ReadOnlyMap 对象根本没有 set 方法,这比在运行时抛出错误更准确。

规范

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

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅