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

瀏覽器相容性

另見