クラス
「クラス」と一言にいってもさまざまであるため、ここでは構造、動作、状態を定義できるものを示すことにします。
また、この章では概念を示す場合はクラスと呼び、クラスに関する構文(記述するコード)のことをclass
構文と呼びます。
クラスとは動作や状態を定義した構造です。
クラスからはインスタンスと呼ばれるオブジェクトを作成でき、インスタンスはクラスに定義した動作を継承し、状態は動作によって変化します。
とても抽象的なことに見えますが、これは今までオブジェクトや関数を使って表現してきたものにも見えます。
実際にJavaScriptではES2015より前まではclass
構文はなく、関数を使いクラスのようなものを表現して扱っていました。
ES2015でクラスを表現するためのclass
構文が導入されましたが、このclass
構文で定義したクラスは関数オブジェクトの一種です。
class
構文ではプロトタイプベースの継承の仕組みの上に関数でクラスを表現しています。
そのため、class
構文はクラスを作るための関数定義や継承をパターン化した書き方といえます。1
JavaScriptでは関数で学んだことの多くは、クラスでもそのまま適応されます。 また、関数の定義方法として関数宣言文と関数式があるように、クラスにもクラス宣言文とクラス式があります。 そのため、関数とクラスは似ている部分が多いです。
この章では、class
構文でのクラスの定義や継承、クラスの性質について学んでいきます。
クラスの定義
クラスを定義するにはclass
構文を使います。
クラスの定義方法にはクラス宣言文とクラス式があります。
まずは、クラス宣言文によるクラスの定義方法を見ていきます。
クラス宣言文ではclass
キーワードを使い、class クラス名{ }
のようにクラスの構造を定義できます。
クラスは必ずコンストラクタを持ち、constructor
という名前のメソッドとして定義します。
コンストラクタとは、そのクラスからインスタンスを作成する際にインスタンスに関する状態の初期化を行うメソッドです。
constructor
メソッドに定義した処理は、クラスをインスタンス化したときに自動的に呼び出されます。
class MyClass {
constructor() {
// コンストラクタ関数の処理
// インスタンス化されるときに自動的に呼び出される
}
}
もうひとつの定義方法であるクラス式は、クラスを値として定義する方法です。 クラス式ではクラス名を省略できます。これは関数式における匿名関数と同じです。
const MyClass = class MyClass {
constructor() {}
};
const AnonymousClass = class {
constructor() {}
};
コンストラクタ関数内で、何も処理がない場合はコンストラクタの記述を省略できます。 省略した場合には自動的に空のコンストラクタが定義されるため、クラスにはコンストラクタが必ず存在します。
class MyClassA {
constructor() {
// コンストラクタの処理が必要なら書く
}
}
// コンストラクタの処理が不要な場合は省略できる
class MyClassB {
}
クラスのインスタンス化
クラスはnew
演算子でインスタンスであるオブジェクトを作成できます。
class
構文で定義したクラスからインスタンスを作成することをインスタンス化と呼びます。
あるインスタンスが指定したクラスから作成されたものかを判定するにはinstanceof
演算子が利用できます。
class MyClass {
}
// `MyClass`をインスタンス化する
const myClass = new MyClass();
// 毎回新しいインスタンス(オブジェクト)を作成する
const myClassAnother = new MyClass();
// それぞれのインスタンスは異なるオブジェクト
console.log(myClass === myClassAnother); // => false
// クラスのインスタンスかどうかは`instanceof`演算子で判定できる
console.log(myClass instanceof MyClass); // => true
console.log(myClassAnother instanceof MyClass); // => true
このままでは何も処理がない空のクラスなので、値を持ったクラスを定義してみましょう。
クラスではインスタンスの初期化処理をコンストラクタ関数で行います。
コンストラクタ関数はnew
演算子でインスタンス化する際に自動的に呼び出されます。
コンストラクタ関数内でのthis
はこれから新しく作るインスタンスオブジェクトとなります。
次のコードでは、x
座標とy
座標の値をもつPoint
というクラスを定義しています。
コンストラクタ関数(constructor
)の中でインスタンスオブジェクト(this
)のx
とy
プロパティに値を代入して初期化しています。
class Point {
// コンストラクタ関数の仮引数として`x`と`y`を定義
constructor(x, y) {
// コンストラクタ関数における`this`はインスタンスを示すオブジェクト
// インスタンスの`x`と`y`プロパティにそれぞれ値を設定する
this.x = x;
this.y = y;
}
}
このPoint
クラスのインスタンスを作成するにはnew
演算子を使います。
new
演算子には関数呼び出しと同じように引数を渡すことができます。
new
演算子の引数はクラスのconstructor
メソッド(コンストラクタ関数)の仮引数に渡されます。
そして、コンストラクタのなかではインスタンスオブジェクト(this
)の初期化処理を行います。
class Point {
// 2. コンストラクタ関数の仮引数として`x`には`3`、`y`には`4`が渡る
constructor(x, y) {
// 3. インスタンス(`this`)の`x`と`y`プロパティにそれぞれ値を設定する
this.x = x;
this.y = y;
// コンストラクタではreturn文は書かない
}
}
// 1. コンストラクタを`new`演算子で引数とともに呼び出す
const point = new Point(3, 4);
// 4. `Point`のインスタンスである`point`の`x`と`y`プロパティには初期化された値が入る
console.log(point.x); // => 3
console.log(point.y); // => 4
このようにクラスからインスタンスを作成するには必ずnew
演算子を使います。
一方、クラスは通常の関数として呼ぶことができません。
これは、クラスのコンストラクタはインスタンス(this
)を初期化する場所であり、通常の関数とは役割が異なるためです。
class MyClass {
constructor() { }
}
// クラスのコンストラクタ関数として呼び出すことはできない
MyClass(); // => TypeError: class constructors must be invoked with |new|
コンストラクタは初期化処理を書く場所であるため、return
文で値を返すべきではありません。
JavaScriptでは、コンストラクタ関数が任意のオブジェクトを返すことが可能ですが、行うべきではありません。
なぜなら、コンストラクタはnew
演算子で呼び出し、その評価結果はクラスのインスタンスを期待するのが一般的であるためです。
// 非推奨の例: コンストラクタで値を返すべきではない
class Point {
constructor(x, y) {
// `this`の代わりにただのオブジェクトを返せる
return { x, y };
}
}
// `new`演算子の結果はコンストラクタ関数が返したただのオブジェクト
const point = new Point(3, 4);
console.log(point); // => { x: 3, y: 4 }
// Pointクラスのインスタンスではない
console.log(point instanceof Point); // => false
[Note] クラス名は大文字で始める
JavaScriptでは慣習としてクラス名は大文字で始まる名前を付けます。 これは、変数名にキャメルケースを使う慣習があるのと同じで、名前自体には特別なルールがあるわけではありません。 クラス名を大文字にしておき、そのインスタンスは小文字で開始すれば名前が被らないという合理的な理由で好まれています。
class Thing {}
const thing = new Thing();
[コラム] class
構文と関数でのクラスの違い
ES2015より前はこれらのクラスをclass
構文ではなく、関数で表現していました。
その表現方法は人によってさまざまで、これもclass
構文という統一した表現が導入された理由の1つです。
次のコードでは、class
構文でのクラスを簡略化した関数での1つの実装例です。
この関数でのクラス表現は、継承の仕組みなどは省かれていますが、class
構文とよく似ています。
// コンストラクタ関数
const Point = function PointConstructor(x, y) {
// インスタンスの初期化処理
this.x = x;
this.y = y;
};
// `new`演算子でコンストラクタ関数から新しいインスタンスを作成
const point = new Point(3, 4);
大きな違いとして、class
構文で定義したクラスは関数として呼び出すことができません。
クラスはnew
演算子でインスタンス化して使うものなので、これはクラスの誤用を防ぐ仕様です。
一方、関数でのクラス表現はただの関数なので、当然関数として呼び出せます。
// 関数でのクラス表現
function MyClassLike() {
}
// 関数なので関数として呼び出せる
MyClassLike();
// `class`構文でのクラス
class MyClass {
}
// クラスは関数として呼び出すと例外が発生する
MyClass(); // => TypeError: class constructors must be invoked with |new|
このように、class
構文で定義したクラスは一種の関数ですが、クラス以外の用途には利用できません。
クラスのプロトタイプメソッドの定義
クラスの動作はメソッドによって定義できます。
constructor
メソッドは初期化時に呼ばれる特殊なメソッドですが、class
構文ではクラスに対して自由にメソッドを定義できます。
このクラスに定義したメソッドは作成したインスタンスがもつ動作となります。
次のようにclass
構文ではクラスに対してメソッドを定義できます。
メソッドの中からクラスのインスタンスを参照するには、constructor
メソッドと同じくthis
を使います。
このクラスのメソッドにおけるthis
は「関数とthis」の章で学んだメソッドと同じくベースオブジェクトを参照します。
class クラス {
メソッド() {
// ここでの`this`はベースオブジェクトを参照
}
}
const インスタンス = new クラス();
// メソッド呼び出しのベースオブジェクト(`this`)は`インスタンス`となる
インスタンス.メソッド();
クラスのプロトタイプメソッド定義では、オブジェクトにおけるメソッドとは異なりkey : value
のように:
区切りでメソッドを定義できないことに注意してください。
つまり、次のような書き方は構文エラー(SyntaxError
)となります。
// クラスでは次のようにメソッドを定義できない
class クラス {
// SyntaxError
メソッド: () => {}
// SyntaxError
メソッド: function(){}
}
このようにクラスに対して定義したメソッドは、クラスの各インスタンスから共有されるメソッドとなります。 このインスタンス間で共有されるメソッドのことをプロトタイプメソッドと呼びます。 また、プロトタイプメソッドはインスタンスから呼び出せるメソッドであるためインスタンスメソッドとも呼ばれます。
この書籍では、プロトタイプメソッド(インスタンスメソッド)をクラス#メソッド名
のように表記します。
次のコードでは、Counter
クラスにincrement
メソッド(Counter#increment
メソッド)を定義しています。
Counter
クラスのインスタンスはそれぞれ別々の状態(count
プロパティ)を持ちます。
class Counter {
constructor() {
this.count = 0;
}
// `increment`メソッドをクラスに定義する
increment() {
// `this`は`Counter`のインスタンスを参照する
this.count++;
}
}
const counterA = new Counter();
const counterB = new Counter();
// `counterA.increment()`のベースオブジェクトは`counterA`インスタンス
counterA.increment();
// 各インスタンスのもつプロパティ(状態)は異なる
console.log(counterA.count); // => 1
console.log(counterB.count); // => 0
またincrement
メソッドはプロトタイプメソッドとして定義されています。
プロトタイプメソッドは各インスタンス間で共有されます。
そのため、次のように各インスタンスのincrement
メソッドの参照先は同じとなっていることが分かります。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
const counterA = new Counter();
const counterB = new Counter();
// 各インスタンスオブジェクトのメソッドは共有されている(同じ関数を参照している)
console.log(counterA.increment === counterB.increment); // => true
プロトタイプメソッドがなぜインスタンス間で共有されているのかは、クラスの継承の仕組みと密接に関係しています。 プロトタイプメソッドの仕組みについては後ほど解説します。
クラスのインスタンスに対してメソッドを定義する
class
構文でのメソッド定義はプロトタイプメソッドとなり、インスタンス間で共有されます。
一方、クラスのインスタンスに対して、直接メソッドを定義する方法もあります。 これは、コンストラクタ関数内でインスタンスオブジェクトに対してメソッドを定義するだけです。
次のコードでは、Counter
クラスのコンストラクタ関数で、インスタンスオブジェクトにincrement
メソッドを定義しています。
コンストラクタ関数内でthis
はインスタンスオブジェクトを示すため、this
に対してメソッドを定義しています。
class Counter {
constructor() {
this.count = 0;
this.increment = () => {
// `this`は`constructor`メソッドにおける`this`(インスタンスオブジェクト)を参照する
this.count++;
};
}
}
const counterA = new Counter();
const counterB = new Counter();
// `counterA.increment()`のベースオブジェクトは`counterA`インスタンス
counterA.increment();
// 各インスタンスのもつプロパティ(状態)は異なる
console.log(counterA.count); // => 1
console.log(counterB.count); // => 0
この方法で定義したincrement
メソッドはインスタンスから呼び出せるため、インスタンスメソッドです。
しかし、インスタンスオブジェクトに定義したincrement
メソッドはプロトタイプメソッドではありません。
インスタンスオブジェクトのメソッドとプロトタイプメソッドには、いくつか異なる点があります。
プロトタイプメソッドは各インスタンスから共有されているため、各インスタンスからのメソッドの参照先が同じでした。 しかし、インスタンスオブジェクトのメソッドは、コンストラクタで毎回同じ挙動の関数(オブジェクト)を新しく定義しています。 そのため、次のように各インスタンスからのメソッドの参照先も異なります。
class Counter {
constructor() {
this.count = 0;
this.increment = () => {
this.count++;
};
}
}
const counterA = new Counter();
const counterB = new Counter();
// 各インスタンスオブジェクトのメソッドの参照先は異なる
console.log(counterA.increment !== counterB.increment); // => true
また、プロトタイプメソッドとは異なり、インスタンスオブジェクトへのメソッド定義はArrow Functionが利用できます。
Arrow Functionにはthis
が静的に決まるという性質があるため、メソッドにおけるthis
の参照先をインスタンスに固定できます。
なぜならArrow Functionで定義したincrement
メソッドはどのような呼び出し方をしても、必ずconstructor
におけるthis
となるためです。(「Arrow Functionでコールバック関数を扱う」を参照)
"use strict";
class ArrowClass {
constructor() {
// コンストラクタでの`this`は常にインスタンス
this.method = () => {
// Arrow Functionにおける`this`は静的に決まる
// そのため`this`は常にインスタンスを参照する
return this;
};
}
}
const instance = new ArrowClass();
const method = instance.method;
// 呼び出し方法(ベースオブジェクト)に依存しないため、`this`がインスタンスを参照する
console.log(method()); // => instance
一方、プロトタイプメソッドにおけるthis
はメソッド呼び出し時のベースオブジェクトを参照します。
そのためプロトタイプメソッドは呼び出し方によってthis
の参照先が異なります。(「関数とthis」の章の「問題: this
を含むメソッドを変数に代入した場合」を参照)
"use strict";
class PrototypeClass {
method() {
// `this`はベースオブジェクトを参照する
return this;
};
}
const instance = new PrototypeClass();
const method = instance.method;
// ベースオブジェクトはundefined
method(); // => undefined
このように、インスタンスに対してArrow Functionでメソッドを定義することでthis
の参照先を固定化できます。
クラスのアクセッサプロパティの定義
クラスに対してメソッドを定義できますが、メソッドはメソッド名()
のように呼び出す必要があります。
クラスでは、プロパティの参照(getter)、プロパティへの代入(setter)時に呼び出される特殊なメソッドを定義できます。
このメソッドはプロパティのように振る舞うためアクセッサプロパティと呼ばれます。
次のコードでは、プロパティの参照(getter)、プロパティへの代入(setter)に対するアクセッサプロパティを定義しています。
アクセッサプロパティはメソッド名(プロパティ名)の前にget
またはset
をつけるだけです。
getter(get
)には仮引数はありませんが、必ず値を返す必要があります。
setter(set
)の仮引数にはプロパティへ代入された値が入りますが、値を返す必要はありません。
class クラス {
// getter
get プロパティ名() {
return 値;
}
// setter
set プロパティ名(仮引数) {
// setterの処理
}
}
const インスタンス = new クラス();
インスタンス.プロパティ名; // getterが呼び出される
インスタンス.プロパティ名 = 値; // setterが呼び出される
次のコードでは、NumberValue#value
をアクセッサプロパティとして定義しています。
number.value
へアクセスした際にそれぞれ定義したgetterとsetterが呼ばれていることが分かります。
このアクセッサプロパティで実際に読み書きされているのは、NumberValue
インスタンスの_value
プロパティとなります。
class NumberValue {
constructor(value) {
this._value = value;
}
// `_value`プロパティの値を返すgetter
get value() {
console.log("getter");
return this._value;
}
// `_value`プロパティに値を代入するsetter
set value(newValue) {
console.log("setter");
this._value = newValue;
}
}
const number = new NumberValue(1);
// "getter"とコンソールに表示される
console.log(number.value); // => 1
// "setter"とコンソールに表示される
number.value = 42;
// "getter"とコンソールに表示される
console.log(number.value); // => 42
[コラム] プライベートプロパティ
NumberValue#value
のアクセッサプロパティで実際に読み書きしているのは、_value
プロパティです。
このように、外から直接読み書きしてほしくないプロパティを_
(アンダーバー)で開始するのはただの習慣であるため、構文としての意味はありません。
現時点(ECMAScript 2018)では、外から原理的に参照できないプライベートプロパティ(hard private)を定義する構文はありません。
しかし、現時点でもWeakSet
などを使うことで擬似的なプライベートプロパティを実現できます。
WeakSet
については「Map/Set」の章で解説します。
Array#length
をアクセッサプロパティで再現する
getterやsetterを利用しないと実現が難しいものとしてArray#length
プロパティがあります。
Array#length
プロパティへ値を代入すると、そのインデックス以降の要素は自動的に削除される仕様があります。
次のコードでは、配列の要素数(length
プロパティ)を小さくすると配列の要素が削除されています。
const array = [1, 2, 3, 4, 5];
// 要素数を減らすと、インデックス以降の要素が削除される
array.length = 2;
console.log(array.join(", ")); // => "1, 2"
// 要素数だけを増やしても、配列の中身は空要素が増えるだけ
array.length = 5;
console.log(array.join(", ")); // => "1, 2, , , "
このlength
プロパティの挙動を再現するArrayLike
クラスを実装してみます。
Array#length
プロパティは、length
プロパティへ値を代入した際に次のようなことを行っています。
- 現在要素数より小さな要素数が指定された場合、その要素数を変更し、配列の末尾の要素を削除する
- 現在要素数より大きな要素数が指定された場合、その要素数だけを変更し、配列の実際の要素はそのままにする
つまり、ArrayLike#length
のsetterで要素の追加や削除を実装することで、配列のようなlength
プロパティを実装できます。
/**
* 配列のようなlengthを持つクラス
*/
class ArrayLike {
constructor(items = []) {
this._items = items;
}
get items() {
return this._items;
}
get length() {
return this._items.length;
}
set length(newLength) {
const currentItemLength = this.items.length;
// 現在要素数より小さな`newLength`が指定された場合、指定した要素数となるように末尾を削除する
if (newLength < currentItemLength) {
this._items = this.items.slice(0, newLength);
} else if (newLength > currentItemLength) {
// 現在要素数より大きな`newLength`が指定された場合、指定した要素数となるように末尾に空要素を追加する
this._items = this.items.concat(new Array(newLength - currentItemLength));
}
}
}
const arrayLike = new ArrayLike([1, 2, 3, 4, 5]);
// 要素数を減らすとインデックス以降の要素が削除される
arrayLike.length = 2;
console.log(arrayLike.items.join(", ")); // => "1, 2"
// 要素数を増やすと末尾に空要素が追加される
arrayLike.length = 5;
console.log(arrayLike.items.join(", ")); // => "1, 2, , , "
このようにアクセッサプロパティは、プロパティのようでありながら実際にアクセスした際には他のプロパティと連動する動作を実現できます。
静的メソッド
インスタンスメソッドは、クラスをインスタンス化して利用します。 一方、クラスをインスタンス化せずに利用できる静的メソッド(クラスメソッド)もあります。
静的メソッドの定義方法はメソッド名の前に、static
をつけるだけです。
class クラス {
static メソッド() {
// 静的メソッドの処理
}
}
// 静的メソッドの呼び出し
クラス.メソッド();
次のコードでは、配列をラップするArrayWrapper
というクラスを定義しています。
ArrayWrapper
はコンストラクタの引数として配列を受け取り初期化しています。
このクラスに配列ではなく要素そのものを引数に受け取りインスタンス化できるArrayWrapper.of
という静的メソッドを定義しています。
class ArrayWrapper {
constructor(array = []) {
this.array = array;
}
// rest parametersとして要素を受け付ける
static of(...items) {
return new ArrayWrapper(items);
}
get length() {
return this.array.length;
}
}
// 配列を引数として渡している
const arrayWrapperA = new ArrayWrapper([1, 2, 3]);
// 要素を引数として渡している
const arrayWrapperB = ArrayWrapper.of(1, 2, 3);
console.log(arrayWrapperA.length); // => 3
console.log(arrayWrapperB.length); // => 3
クラスの静的メソッドにおけるthis
は、そのクラス自身を参照します。
そのため、先ほどのコードはnew ArrayWrapper
の代わりにnew this
と書くこともできます。
class ArrayWrapper {
constructor(array = []) {
this.array = array;
}
static of(...items) {
// `this`は`ArrayWrapper`を参照する
return new this(items);
}
get length() {
return this.array.length;
}
}
const arrayWrapper = ArrayWrapper.of(1, 2, 3);
console.log(arrayWrapper.length); // => 3
このように静的メソッドでのthis
はクラス自身を参照するため、クラスのインスタンスは参照できません。
そのため静的メソッドは、クラスのインスタンスを作成する処理やクラスに関係する処理を書くために利用されます。
2種類のインスタンスメソッドの定義
クラスでは、2種類のインスタンスメソッドの定義方法があります。
class
構文を使ったインスタンス間で共有されるプロトタイプメソッドの定義と、
インスタンスオブジェクトに対するメソッドの定義です。
これらの2つの方法を同時に使い、1つのクラスに同じ名前でメソッドを2つ定義した場合はどうなるでしょうか?
次のコードでは、ConflictClass
クラスにプロトタイプメソッドとインスタンスに対して同じmethod
という名前のメソッドを定義しています。
class ConflictClass {
constructor() {
// インスタンスオブジェクトに`method`を定義
this.method = () => {
console.log("インスタンスオブジェクトのメソッド");
};
}
// クラスのプロトタイプメソッドとして`method`を定義
method() {
console.log("プロトタイプのメソッド");
}
}
const conflict = new ConflictClass();
conflict.method(); // どちらの`method`が呼び出される?
結論から述べるとこの場合はインスタンスオブジェクトに定義したmethod
が呼び出されます。
このとき、インスタンスのmethod
プロパティをdelete
演算子で削除すると、今度はプロトタイプメソッドのmethod
が呼び出されます。
class ConflictClass {
constructor() {
this.method = () => {
console.log("インスタンスオブジェクトのメソッド");
};
}
method() {
console.log("プロトタイプメソッド");
}
}
const conflict = new ConflictClass();
conflict.method(); // "インスタンスオブジェクトのメソッド"
// インスタンスの`method`プロパティを削除
delete conflict.method;
conflict.method(); // "プロトタイプのメソッド"
この実行結果から次のことが分かります。
- プロトタイプメソッドとインスタンスオブジェクトのメソッドは上書きされずにどちらも定義されている
- インスタンスオブジェクトのメソッドがプロトタイプオブジェクトのメソッドよりも優先して呼ばれている
どちらも注意深く意識しないと気づきにくいですが、この挙動はJavaScriptの重要な仕組みであるため理解することは重要です。
この挙動はプロトタイプオブジェクトと呼ばれる特殊なオブジェクトとプロトタイプチェーンと呼ばれる仕組みで成り立っています。 どちらもプロトタイプとついていることからも分かるように、2つで1組のような仕組みです。
このセクションでは、プロトタイプオブジェクトとプロトタイプチェーンとはどのような仕組みなのかを見ていきます。
プロトタイプオブジェクト
プロトタイプメソッドとインスタンスオブジェクトのメソッドを同時に定義しても、互いのメソッドは上書きされるわけでありません。 なぜなら、プロトタイプメソッドはプロトタイプオブジェクトへ、インスタンスオブジェクトのメソッドはインスタンスオブジェクトへそれぞれ定義されるためです。
プロトタイプオブジェクトとは、JavaScriptの関数オブジェクトのprototype
プロパティに自動的に作成される特殊なオブジェクトです。
クラスも一種の関数オブジェクトであるため、自動的にprototype
プロパティにプロトタイプオブジェクトが作成されています。
次のコードでは、関数やクラス自身のprototype
プロパティに、プロトタイプオブジェクトが自動的に作成されていることが分かります。
function fn() {
}
// `prototype`プロパティにプロトタイプオブジェクトが存在する
console.log(typeof fn.prototype === "object"); // => true
class MyClass {
}
// `prototype`プロパティにプロトタイプオブジェクトが存在する
console.log(typeof MyClass.prototype === "object"); // => true
class
構文のメソッド定義は、このプロトタイプオブジェクトのプロパティとして定義されます。
次のコードでは、クラスのメソッドがプロトタイプオブジェクトに定義されていることを確認できます。
また、クラスにはconstructor
メソッド(コンストラクタ)が必ず定義されます。
このconstructor
メソッドもプロトタイプオブジェクトに定義されており、このconstructor
プロパティはクラス自身を参照します。
class MyClass {
method() { }
}
console.log(typeof MyClass.prototype.method === "function"); // => true
// クラス#constructorはクラス自身を参照する
console.log(MyClass.prototype.constructor === MyClass); // => true
このように、プロトタイプメソッドはプロトタイプオブジェクトに定義され、インスタンスオブジェクトのメソッドとは異なるオブジェクトに定義されています。そのため、それぞれの方法でメソッドを定義しても、上書きされずにそれぞれ異なるオブジェクトへ定義されます。
プロトタイプチェーン
class
構文で定義したプロトタイプメソッドはプロトタイプオブジェクトに定義されます。
しかし、インスタンス(オブジェクト)にはメソッドが定義されていないのに、インスタンスからクラスのプロトタイプメソッドを呼び出すことができます。
class MyClass {
method() {
console.log("プロトタイプのメソッド");
}
}
const instance = new MyClass();
instance.method(); // "プロトタイプのメソッド"
このインスタンスからプロトタイプメソッドを呼び出せるのはプロトタイプチェーンと呼ばれる仕組みによるものです。 プロトタイプチェーンは2つの処理から成り立ちます。
- インスタンス作成時に、インスタンスの
[[Prototype]]
内部プロパティへプロトタイプオブジェクトの参照を保存する処理 - インスタンスからプロパティ(またはメソッド)を参照する時に、
[[Prototype]]
内部プロパティまで探索する処理
インスタンス作成とプロトタイプチェーン
クラスからnew
演算子によってインスタンスを作成する際に、インスタンスにはクラスのプロトタイプオブジェクトの参照が保存されます。
このとき、インスタンスからクラスのプロトタイプオブジェクトへの参照は、インスタンスオブジェクトの[[Prototype]]
という内部プロパティに保存されます。
[[Prototype]]
内部プロパティはECMAScriptの仕様で定められた内部的な表現であるため、通常のプロパティのようにはアクセスできません。
しかし、Object.getPrototypeOf(object)
メソッドでobject
の[[Prototype]]
内部プロパティを読み取れます。
次のコードでは、インスタンスの[[Prototype]]
内部プロパティを取得しています。
その取得した結果がクラスのプロトタイプオブジェクトを参照していることを確認できます。
class MyClass {
method() {
console.log("プロトタイプのメソッド");
}
}
const instance = new MyClass();
// instanceの`[[Prototype]]`内部プロパティは`MyClass.prototype`と一致する
const Prototype = Object.getPrototypeOf(instance);
console.log(Prototype === MyClass.prototype); // => true
ここで重要なのは、インスタンスはどのクラスから作られたかやそのクラスのプロトタイプオブジェクトを知っているということです。
Note: [[Prototype]]
内部プロパティを読み書きする
Object.getPrototypeOf(object)
でobject
の[[Prototype]]
を読み取ることができます。
一方、Object.setPrototypeOf(object, prototype)
でobject
の[[Prototype]]
にprototype
オブジェクトを書き込めます。
また、[[Prototype]]
内部プロパティを通常のプロパティのように扱える__proto__
という特殊なアクセッサプロパティが存在します。
しかし、これらの[[Prototype]]
内部プロパティを直接読み書きすることは通常の用途では行いません。
また、既存のビルトインオブジェクトの動作なども変更できるため、不用意に扱うべきではないでしょう。
プロパティの参照とプロトタイプチェーン
プロトタイプオブジェクトのプロパティがどのようにインスタンスから参照されるかを見ていきます。
オブジェクトのプロパティを参照するときに、オブジェクト自身がプロパティを持っていない場合でも、そこで探索が終わるわけではありません。
オブジェクトの[[Prototype]]
内部プロパティの参照先であるプロトタイプオブジェクトに対しても探索を続けます。
これは、スコープに指定した識別子の変数がなかった場合に外側のスコープへと探索するスコープチェーンと良く似た仕組みです。
つまり、オブジェクトがプロパティを探索するときは次のような順番で、それぞれのオブジェクトを調べます。
すべてのオブジェクトにおいて見つからなかった場合の結果はundefined
を返します。
instance
オブジェクト自身instance
オブジェクトの[[Prototype]]
の参照先(プロトタイプオブジェクト)- どこにもなかった場合は
undefined
次のコードでは、インスタンスオブジェクト自身はmethod
プロパティを持っていません。
そのため、実際参照してるのはクラスのプロトタイプオブジェクトのmethod
プロパティです。
class MyClass {
method() {
console.log("プロトタイプのメソッド");
}
}
const instance = new MyClass();
// インスタンスには`method`プロパティがないため、プロトタイプオブジェクトの`method`が参照される
instance.method(); // "プロトタイプのメソッド"
// `instance.method`の参照はプロトタイプオブジェクトの`method`と一致する
const Prototype = Object.getPrototypeOf(instance);
console.log(instance.method === Prototype.method); // => true
このように、インスタンス(オブジェクト)にmethod
が定義されていなくても、クラスのプロトタイプオブジェクトのmethod
を呼び出すことができます。
このプロパティを参照する際に、オブジェクト自身から[[Prototype]]
内部プロパティへと順番に探す仕組みのことをプロトタイプチェーンと呼びます。
プロトタイプチェーンの仕組みを擬似的なコードとして表現すると次のような動きをしています。
// プロトタイプチェーンの擬似的な動作の擬似的なコード
class MyClass {
method() {
console.log("プロトタイプのメソッド");
}
}
const instance = new MyClass();
// `instance.method()`を実行する場合
// 次のような呼び出し処理が行われている
// インスタンス自身が`method`プロパティを持っている場合
if (instance.hasOwnProperty("method")) {
instance.method();
} else {
// インスタンスの`[[Prototype]]`の参照先(`MyClass`のプロトタイプオブジェクト)を取り出す
const prototypeObject = Object.getPrototypeOf(instance);
// プロトタイプオブジェクトが`method`プロパティを持っている場合
if (prototypeObject.hasOwnProperty("method")) {
// `this`はインスタンス自身を指定して呼び出す
prototypeObject.method.call(instance);
}
}
プロトタイプチェーンの仕組みによって、プロトタイプオブジェクトに定義したプロトタイプメソッドがインスタンスから呼び出すことができています。
普段は、プロトタイプオブジェクトやプロトタイプチェーンといった仕組みを意識する必要はありません。
class
構文はこのようなプロトタイプを意識せずにクラスを利用できるように導入された構文です。
しかし、プロトタイプベースである言語のJavaScriptではクラスをこのようなプロトタイプを使い表現していることは知っておくとよいでしょう。
継承
extends
キーワードを使うことで既存のクラスを継承できます。
継承とは、クラスの構造や機能を引き継いだ新しいクラスを定義することです。
継承したクラスの定義
extends
キーワードを使って既存のクラスを継承した新しいクラスを定義してみます。
class
構文の右辺にextends
キーワードで継承元となる親クラス(基底クラス)を指定することで、
親クラスを継承した子クラス(派生クラス)を定義できます。
class 子クラス extends 親クラス {
}
次のコードでは、Parent
クラスを継承したChild
クラスを定義しています。
子クラスであるChild
クラスのインスタンス化は通常のクラスと同じくnew
演算子を使って行います。
class Parent {
}
class Child extends Parent {
}
const instance = new Child();
super
extends
を使って定義した子クラスから親クラスを参照するにはsuper
というキーワードを利用します。
もっともシンプルなsuper
を使う例としてコンストラクタの処理を見ていきます。
class
構文でも紹介しましたが、クラスは必ずconstructor
メソッド(コンストラクタ)をもちます。
これは、継承した子クラスでも同じです。
次のコードでは、Parent
クラスを継承したChild
クラスのコンストラクタで、super()
を呼び出しています。
super()
は子クラスから親クラスのconstructor
メソッドを呼び出します。
// 親クラス
class Parent {
constructor(...args) {
console.log("Parentコンストラクタの処理", ...args);
}
}
// Parentを継承したChildクラスの定義
class Child extends Parent {
constructor(...args) {
// Parentのコンストラクタ処理を呼びだす
super(...args);
console.log("Childコンストラクタの処理", ...args);
}
}
const child = new Child("引数1", "引数2");
// "Parentコンストラクタの処理", "引数1", "引数2"
// "Childコンストラクタの処理", "引数1", "引数2"
class
構文でのクラス定義では、constructor
メソッド(コンストラクタ)で何も処理しない場合は省略できることを紹介しました。
これは、継承した子クラスでも同じです。
次のコードのChild
クラスのコンストラクタでは、何も処理を行っていません。
そのため、Child
クラスのconstructor
メソッドの定義を省略できます。
class Parent {}
class Child extends Parent {}
このように子クラスでconstructor
を省略した場合は次のように書いた場合と同じ意味になります。
constructor
メソッドの引数をすべて受け取り、そのままsuper
へ引数の順番を維持して渡します。
class Parent {}
class Child extends Parent {
constructor(...args) {
super(...args); // 親クラスに引数をそのまま渡す
}
}
コンストラクタの処理順は親クラスから子クラスへ
コンストラクタの処理順は、親クラスから子クラスへと順番が決まっています。
class
構文ではかならず親クラスのコンストラクタ処理(super()
の呼び出し)を先に行い、その次に子クラスのコンストラクタ処理を行います。
子クラスのコンストラクタでは、this
を触る前にsuper()
で親クラスのコンストラクタ処理を呼び出さないとSyntaxError
となるためです。
次のコードでは、Parent
とChild
でそれぞれインスタンス(this
)のname
プロパティに値を書き込んでいます。
子クラスでは先にsuper()
を呼び出してからでないとthis
を参照できません。
そのため、コンストラクタの処理順はParent
からChild
という順番に限定されます。
class Parent {
constructor() {
this.name = "Parent";
}
}
class Child extends Parent {
constructor() {
// 子クラスでは`super()`を`this`に触る前に呼び出さなければならない
super();
// 子クラスのコンストラクタ処理
// 親クラスで書き込まれた`name`は上書きされる
this.name = "Child";
}
}
const parent = new Parent();
console.log(parent.name); // => "Parent"
const child = new Child();
console.log(child.name); // => "Child"
プロトタイプ継承
次のコードではextends
キーワードを使いParent
クラスを継承したChild
クラスを定義しています。
Parent
クラスではmethod
を定義しているため、これを継承しているChild
クラスのインスタンスからも呼び出せます。
class Parent {
method() {
console.log("Parent#method");
}
}
// `Parent`を継承した`Child`を定義
class Child extends Parent {
// methodの定義はない
}
// `Child`のインスタンスは`Parent`のプロトタイプメソッドを継承している
const instance = new Child();
instance.method(); // "Parent#method"
このように、子クラスのインスタンスから親クラスのプロトタイプメソッドもプロトタイプチェーンの仕組みによって呼びだせます。
extends
によって継承した場合、子クラスのプロトタイプオブジェクトの[[Prototype]]
内部プロパティには親クラスのプロトタイプオブジェクトが設定されます。
このコードでは、Child.prototype
オブジェクトの[[Prototype]]
内部プロパティにはParent.prototype
が設定されます。
これにより、プロパティを参照する場合には次のような順番でオブジェクトを探索しています。
instance
オブジェクト自身Child.prototype
(instance
オブジェクトの[[Prototype]]
の参照先)Parent.prototype
(Child.prototype
オブジェクトの[[Prototype]]
の参照先)
このプロトタイプチェーンの仕組みより、method
プロパティはParent.prototype
オブジェクトに定義されたものを参照します。
このようにJavaScriptではclass
構文とextends
キーワードを使うことでクラスの機能を継承できます。
class
構文ではプロトタイプオブジェクトと参照する仕組みによって継承が行われています。
そのため、この継承の仕組みをプロトタイプ継承と呼びます。
静的メソッドの継承
インスタンスはクラスのプロトタイプオブジェクトとの間にプロトタイプチェーンがあります。 クラス自身(クラスのコンストラクタ)も親クラス自身(親クラスのコンストラクタ)との間にプロトタイプチェーンがあります。
これは簡単にいえば、静的メソッドも継承されるということです。
class Parent {
static hello() {
return "Hello";
}
}
class Child extends Parent {}
console.log(Child.hello()); // => "Hello"
extends
によって継承した場合、子クラスのコンストラクタの[[Prototype]]
内部プロパティには親クラスのコンストラクタが設定されます。
このコードでは、Child
コンストラクタの[[Prototype]]
内部プロパティにParent
コンストラクタが設定されます。
つまり、先ほどのコードではChild.hello
プロパティを参照した場合には、次のような順番でオブジェクトを探索しています。
Child
コンストラクタParent
コンストラクタ(Child
コンストラクタの[[Prototype]]
の参照先)
クラスのコンストラクタ同士にもプロトタイプチェーンの仕組みがあるため、子クラスは親クラスの静的メソッドを呼び出せます。
super
プロパティ
子クラスから親クラスのコンストラクタ処理を呼び出すにはsuper()
を使います。
同じように、子クラスのプロトタイプメソッドからは、super.プロパティ名
で親クラスのプロトタイプメソッドを参照できます。
次のコードでは、Child#method
の中でsuper.method()
と書くことでParent#method
を呼び出しています。
このように、子クラスから継承元の親クラスのプロトタイプメソッドはsuper.プロパティ名
で参照できます。
class Parent {
method() {
console.log("Parent#method");
}
}
class Child extends Parent {
method() {
console.log("Child#method");
// `this.method()`だと自分(`this`)のmethodを呼び出して無限ループする
// そのため明示的に`super.method()`とParent#methodを呼びだす
super.method();
}
}
const child = new Child();
child.method();
// コンソールには次のように出力される
// "Child#method"
// "Parent#method"
プロトタイプチェーンでは、インスタンスからクラス、さらに親のクラスと継承関係をさかのぼるようにメソッドを探索すると紹介しました。
このコードではChild#method
が定義されているため、child.method
はChild#method
を呼び出します。
そしてChild#method
はsuper.method
を呼び出しているため、Parent#method
が呼び出されます。
クラスの静的メソッド同士も同じようにsuper.method()
と書くことで呼び出せます。
次のコードでは、Parent
を継承したChild
から親クラスの静的メソッドを呼び出しています。
class Parent {
static method() {
console.log("Parent.method");
}
}
class Child extends Parent {
static method() {
console.log("Child.method");
// `super.method()`で`Parent.method`を呼びだす
super.method();
}
}
Child.method();
// コンソールには次のように出力される
// "Child.method"
// "Parent.method"
継承の判定
あるクラスが指定したクラスをプロトタイプ継承しているかはinstanceof
演算子を使って判定できます。
次のコードでは、Child
のインスタンスはChild
クラスとParent
クラスを継承したオブジェクトであることを確認しています。
class Parent {}
class Child extends Parent {}
const parent = new Parent();
const child = new Child();
// `Parent`のインスタンスは`Parent`のみを継承したインスタンス
console.log(parent instanceof Parent); // => true
console.log(parent instanceof Child); // => false
// `Child`のインスタンスは`Child`と`Parent`を継承したインスタンス
console.log(child instanceof Parent); // => true
console.log(child instanceof Child); // => true
より具体的な継承の使い方については「ユースケース:Todoアプリ」の章で見ていきます。
ビルトインオブジェクトの継承
ここまで自身が定義したクラスを継承してきましたが、ビルトインオブジェクトのコンストラクタも継承できます。
ビルトインオブジェクトにはArray
、String
、Object
、Number
、Error
、Date
などのコンストラクタがあります。
class
構文ではこれらのビルトインオブジェクトを継承できます。
次のコードでは、ビルトインオブジェクトであるArray
を継承して独自のメソッドを加えたMyArray
クラスを定義しています。
継承したMyArray
はArray
の性質であるメソッドや状態管理についての仕組みを継承しています。
継承した性質に加えて、MyArray#first
やMyArray#last
といったアクセッサプロパティを追加しています。
class MyArray extends Array {
get first() {
if (this.length === 0) {
return undefined;
} else {
return this[0];
}
}
get last() {
if (this.length === 0) {
return undefined;
} else {
return this[this.length - 1];
}
}
}
// Arrayを継承しているのでArray.fromも継承している
// Array.fromはIterableなオブジェクトから配列インスタンスを作成する
const array = MyArray.from([1, 2, 3, 4, 5]);
console.log(array.length); // => 5
console.log(array.first); // => 1
console.log(array.last); // => 5
Array
を継承したMyArray
は、Array
が元々もつlength
プロパティやArray.from
メソッドなどを継承し利用できます。
1.class
構文でのみしか実現できない機能はなく、読みやすさや分かりやさのために導入された構文という側面もあるため、JavaScriptのclass
構文は糖衣構文(シンタックスシュガー)と呼ばれることがあります。 ↩