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.
試してみましょう
構文
class ChildClass extends ParentClass { /* … */ }
ParentClass
-
コンストラクター関数(クラスを含む)または
null
と評価される式。
解説
extends
キーワードは、組み込みオブジェクトと同様にカスタムクラスをサブクラス化するために使用することができます。
new
で呼び出すことができ、 prototype
プロパティを持つコンストラクターであれば、親クラスの候補になることができます。例えば、バインド済み関数や Proxy
は構築可能ですが、これらは prototype
プロパティを持たないので、サブクラス化できません。
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
演算子では無視されます。)
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
の両方のプロトタイプを設定します。
ChildClass のプロトタイプ |
ChildClass.prototype のプロトタイプ |
|
---|---|---|
extends 節がない |
Function.prototype |
Object.prototype |
extends null |
Function.prototype |
null |
extends ParentClass |
ParentClass |
ParentClass.prototype |
class ParentClass {}
class ChildClass extends ParentClass {}
// 静的プロパティの継承が可能
Object.getPrototypeOf(ChildClass) === ParentClass;
// インスタンスプロパティの継承が可能
Object.getPrototypeOf(ChildClass.prototype) === ParentClass.prototype;
extends
の右辺は識別子である必要はありません。コンストラクターとして評価される式なら何でも使用することができます。これはミックスインを作成するのに有益なことが多いです。 extends
式の this
値はクラス定義の外側の this
であり、このクラスは初期化されていないので、クラス名を参照すると ReferenceError
になります。この式では await
および yield
は期待通りに動作します。
class SomeClass extends class {
constructor() {
console.log("基底クラス");
}
} {
constructor() {
super();
console.log("派生クラス");
}
}
new SomeClass();
// 基底クラス
// 派生クラス
基底クラスはコンストラクターから何らかのものを返すことができますが、派生クラスはオブジェクトを返すか undefined
を返さなければなりません。
class ParentClass {
constructor() {
return 1;
}
}
console.log(new ParentClass()); // ParentClass {}
// オブジェクトではないため、返値は無視される
// これは関数コンストラクターと整合する
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
内のthis
が未定義であるためにエラーを発生することになります。これを修正する方法としては、Array.from()
がそうであるように、this
がコンストラクターでない場合に基底クラスで代替処理をすることですが、それでも基底クラスが特殊ケースであることを意味しています。 - 2 つ目は、インスタンスメソッドがコンストラクター関数を取得するために
this.constructor
を読み込むことを要求されることです。しかし、コンストラクターのプロパティは書き込みと設定の両方が可能であり、保護されていないため、new this.constructor()
は古いコードを壊す可能性があります。そのため、組み込みメソッドをコピーする場合の多くは、代わりにコンストラクターの[Symbol.species]
プロパティを使用しています(既定では、単にコンストラクター自身であるthis
を返します)。しかし、[Symbol.species]
によって任意のコードを実行したり、任意のタイプのインスタンスを作成したりすることができるため、セキュリティ上の問題があり、サブクラスの意味づけが非常に複雑になります。 - 3 つ目はカスタムコードを目に見える形で呼び出すことになり、多くのオプティマイザーの実装が難しくなります。例えば、
Map()
コンストラクターが x 要素の反復可能オブジェクトで呼び出された場合、内部ストレージに要素をコピーするだけではなく、目に見える形でset()
メソッドを x 回呼び出さなければなりません。
これらの問題は組み込みクラスに固有のものではありません。自分自身で作成したクラスについても、同じような決定をしなければならないことがあるでしょう。しかし、組み込みクラスでは、最適化とセキュリティがより大きな関心事です。新しい組み込みメソッドは常に基底クラスを構築し、可能な限りいくつかのカスタムメソッドを呼び出します。上記の期待値を達成しながら組み込みクラスをサブクラス化したい場合は、既定値の動作が組み込まれているメソッドをすべてオーバーライドする必要があります。既定では継承されているため、基底クラスに新しいメソッドを追加すると、サブクラスの意味づけが崩れる可能性があります。したがって、組み込みクラスを拡張するためのより良い方法は、コンポジション を使用することです。
null を拡張
extends null
は、Object.prototype
を継承しないオブジェクトを簡単に作成できるようにするために設計されました。しかし、コンストラクター内で super()
を呼び出すべきかどうかが未確定なため、オブジェクトを返さないコンストラクターの実装を使用して、実際にそのようなクラスを構築することは可能ではありません。 TC39 委員会はこの機能を再び使えるようにするために作業しています。
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
代わりに、コンストラクターから明示的にインスタンスを返す必要があります。
class NullClass extends null {
constructor() {
// new.target を使用することで、派生クラスが正しいプロトタイプチェーンを
// 持つことができる
return Object.create(new.target.prototype);
}
}
const proto = Object.getPrototypeOf;
console.log(proto(proto(new NullClass()))); // null
例
extends の使用
最初の例では、 Square
と呼ばれるクラスを Polygon
と呼ばれるクラスから作成します。この例は、ライブデモ(ソース)から転載しています。
class Square extends Polygon {
constructor(length) {
// ここでは、親クラスのコンストラクターを呼び出し、
// Polygon の幅と高さの寸法を渡します。
super(length, length);
// 注: 派生クラスでは、 'this' を使う前に super() を
// 呼び出さなくてはなりません。さもないと参照エラーになります。
this.name = "Square";
}
get area() {
return this.height * this.width;
}
}
プレーンなオブジェクトの拡張
クラスは通常の(構築不可能な)オブジェクトを継承することはできません。このオブジェクトのすべてのプロパティを継承したインスタンスで利用できるようにして、通常のオブジェクトを継承したい場合は、代わりに Object.setPrototypeOf()
を使用することができます。
const Animal = {
speak() {
console.log(`${this.name}が鳴きます。`);
},
};
class Dog {
constructor(name) {
this.name = name;
}
}
Object.setPrototypeOf(Dog.prototype, Animal);
const d = new Dog("ミッチー");
d.speak(); // ミッチーが鳴きます。
組み込みオブジェクトでの extends の使用
Object
の拡張
JavaScript のオブジェクトはすべて既定では Object.prototype
を継承しているので、一見すると extends Object
と書くのは冗長に見えます。 extends
をまったく書かない場合と異なる形で言えば、コンストラクター自体が Object.keys()
のような静的メソッドを Object
から継承していることくらいです。しかし、どの Object
の静的メソッドも this
の値を使用していないため、これらの静的メソッドを継承することに有益な値はありません。
Object()
コンストラクターはサブクラスのシナリオを特殊化します。 super()
によって暗黙的に呼び出された場合、常に new.target.prototype
をプロトタイプとする新しいオブジェクトを初期化します。 super()
に渡す値は無視されます。
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' ]
この動作を、サブクラスを特殊化しないカスタムラッパーと比較してみてください。
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
オブジェクトを返したい場合もあります。 species パターンを使うと、既定のコンストラクターを上書きすることができます。
例えば、Array.prototype.map()
のような既定のコンストラクターを返すメソッドを使用する場合、これらのメソッドは MyArray
オブジェクトの代わりに、親の Array
オブジェクトを返すようにします。シンボル Symbol.species
を使用すると、これを行うことができます。
class MyArray extends Array {
// 親の Array コンストラクターの species を上書き
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
この動作は、多くの組み込みコピーメソッドで実装されています。この機能の注意点については、組み込みクラスのサブクラス化の説明を参照してください。
ミックスイン
抽象サブクラスまたはミックスインは、クラスのテンプレートです。クラスはスーパークラスを 1 つしか持つことができないので、例えばツールクラスからの多重継承は不可能です。機能はスーパークラスが提供しなければなりません。
スーパークラスを入力とし、そのスーパークラスを拡張したサブクラスを出力とする関数が、ミックスインを実装するために使用することができます。
const calculatorMixin = (Base) =>
class extends Base {
calc() {}
};
const randomizerMixin = (Base) =>
class extends Base {
randomize() {}
};
これらのミックスインを使用するクラスは、次のように書くことができます。
class Foo {}
class Bar extends calculatorMixin(randomizerMixin(Foo)) {}
継承を避ける
オブジェクト指向プログラミングにおいて、継承はとても強い結合関係です。これは基底クラスのすべての振る舞いが既定でサブクラスに継承されることを意味します。例えば、 ReadOnlyMap
の実装を考えてみましょう。
class ReadOnlyMap extends Map {
set() {
throw new TypeError("A read-only map must be set at construction time.");
}
}
Map()
コンストラクターはインスタンスの set()
メソッドを呼び出すからです。
const m = new ReadOnlyMap([["a", 1]]); // TypeError: A read-only map must be set at construction time.
インスタンスが構築済みかどうかを示すためにプライベートなフラグを使用することで、これを回避することができます。しかし、この設計のより重大な問題は、リスコフの置換原則を破ってしまうことです。これは、サブクラスはスーパークラスと置換可能であるべきだという状態です。もし関数が Map
オブジェクトを期待するのであれば、 ReadOnlyMap
オブジェクトも使用することができるはずです。
継承はしばしば円-楕円問題を引き起こします。なぜなら、どちらの型も、多くの共通の特徴を共有しているにもかかわらず、他の型の振る舞いを完全に内包していないからです。一般的に、継承を使用するとてもよい理由がない限り、代わりにコンポジションを使用する方がよいでしょう。コンポジションとは、あるクラスが他のクラスのオブジェクトへの参照を持っていて、そのオブジェクトを実装の詳細としてのみ使用していることを意味しています。
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()
メソッドを追加した場合、後者が emplace()
もオーバーライドするように更新されない限り、 ReadOnlyMap
クラスは読み取り専用ではなくなります。さらに、ReadOnlyMap
オブジェクトは set
メソッドをすべて持たないので、実行時にエラーを発生するよりも正確です。
仕様書
Specification |
---|
ECMAScript Language Specification # sec-class-definitions |
ブラウザーの互換性
BCD tables only load in the browser