JavaScriptのクラス構文
クラスベース言語との対比
JavaScriptは本質的にプロトタイプベースの言語ですが、ES2015(ES6)以降では他のクラスベース言語に似た表記ができるクラス構文が導入されました。
これによって、Javaなどと同様にクラスを用いて直感的にオブジェクト指向プログラミングを行うことができます。
Javaではクラス定義の前にメンバー変数をあらかじめ宣言する必要がありますが、JavaScriptではコンストラクタの内部などで this. に値を代入することで動的にプロパティが追加されます。
Javaの「フィールド」に相当するものは、JavaScriptではプロパティと呼びます。
Javaの「メソッド」に相当するものは、JavaScriptでもメソッドと呼びます。
JavaScriptでクラスを定義する基本的なプログラムは以下のようになります。
point.js
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const point = new Point(10, 20);
console.log(point.x);
console.log(point.y);
実行結果
10
20
JavaScriptでは new キーワードを使用してインスタンスを生成します。
生成時には、自動的にクラス内の constructor メソッドが呼び出されます。
コンストラクタの中で this.x や this.y のように指定して代入を行うことで、インスタンスにプロパティが追加されます。
メソッドの定義とクラスの継承
クラスの内部に処理を記述したメソッドを定義する場合、function キーワードは省略します。
また、既存のクラスを元にして新しいクラスを作る継承を行うには、extends キーワードを使用します。
これはJavaにおける継承と同じ方法です。
メソッドの定義と継承を行うプログラムは以下のようになります。
inheritance.js
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log("音を出します");
}
}
class Dog extends Animal {
makeSound() {
console.log(`${this.name}がワンワンと鳴きます`);
}
}
const dog = new Dog("ポチ");
dog.makeSound();
実行結果
ポチがワンワンと鳴きます
子クラスにおいて親クラスと同名のメソッドを再定義することで、処理を上書きするオーバーライドが実現できます。
子クラスのコンストラクタやメソッドから親クラスの処理を呼び出したい場合は、super キーワードを使用します。
子クラスでコンストラクタを定義する場合は、this にアクセスする前に必ず super() を呼び出す必要があります。
プライベートフィールドとカプセル化
以前のJavaScriptには、Javaの private に相当する厳密なアクセス制限の機能がありませんでした。
そのため、外部から直接書き換えられたくないプロパティには _x のように名前の先頭にアンダースコアを付けることで、アクセスしないよう開発者間の慣例でカプセル化を実現していました。
しかし、現在のJavaScriptでは言語機能としてプライベートフィールドがサポートされています。
名前の先頭にハッシュ記号(#)を付けることで、クラスの外部からは一切アクセスできないように制限できます。
プライベートフィールドを使用するプログラムは以下のようになります。
private_field.js
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
printPosition() {
console.log(`位置: (${this.#x}, ${this.#y})`);
}
}
const point = new Point(10, 20);
point.printPosition();
try {
console.log(point.#x);
} catch (error) {
console.log("外部からアクセスできません");
}
実行結果
位置: (10, 20)
外部からアクセスできません
プライベートフィールドを使用する場合は、あらかじめクラスの先頭でハッシュ記号を付けた名前を宣言しておく必要があります。
コンストラクタやメソッドの内部からアクセスする際も、常にハッシュ記号を付けた this.#x のように指定する必要があります。
クラスの外部からアクセスしようとすると構文エラーまたは実行時エラーが発生するため、厳密なカプセル化を行うことができます。
ゲッターとセッター
プライベートフィールドによってデータを隠蔽した上で、値の取得や変更を行うための窓口としてゲッターとセッターを定義できます。
これ利用することで、外部からは通常のプロパティにアクセスする感覚で、内部的な値の検証処理などを実行させることができます。
ゲッターは get キーワード、セッターは set キーワードを使用してメソッドのように定義します。
ゲッターとセッターを利用するプログラムは以下のようになります。
getter_setter.js
class Account {
#balance = 0;
get balance() {
return this.#balance;
}
set balance(amount) {
if (amount < 0) {
console.log("残高を負の値にすることはできません");
return;
}
this.#balance = amount;
}
}
const account = new Account();
account.balance = 1000;
console.log(account.balance);
account.balance = -500;
console.log(account.balance);
実行結果
1000
残高を負の値にすることはできません
1000
セッターを呼び出す際はメソッド呼び出しの括弧を書く必要はなく、account.balance = 1000 のように代入式として記述します。
ゲッターを呼び出す際も同様に、プロパティのように参照するだけで自動的にメソッドが実行されます。
これによって、安全なカプセル化と簡潔なアクセス表記を両立させることができます。
静的メソッドと静的プロパティ
JavaScriptのクラス構文では、static キーワードを使用することで、インスタンスを生成せずにクラスから直接アクセスできる静的プロパティや静的メソッドを定義できます。
これらは、個別のインスタンスに依存しないデータや機能(共通の定数やユーティリティ関数など)をまとめるのに役立ちます。
静的メソッドの内部では、this はインスタンスではなくクラス自身を指し示します。
静的プロパティと静的メソッドを利用するプログラムは以下のようになります。
static_member.js
class MathUtility {
static PI = 3.14159;
static calculateCircumference(radius) {
// クラスの静的プロパティへアクセスするため this を使用する
return 2 * this.PI * radius;
}
}
console.log(MathUtility.PI);
console.log(MathUtility.calculateCircumference(5));
実行結果
3.14159
31.4159
このように、インスタンスを生成するための new を行うことなく、MathUtility.PI や MathUtility.calculateCircumference(5) の形でプロパティの取得やメソッドの呼び出しを行うことができます。
演習
演習1
プライベートフィールド # を使用して、内部状態を外部から直接変更できないようにカプセル化した簡易的なキーバリューストアクラスである SimpleMap を作成しなさい。
本クラスは、以下の仕様を満たすように設計しなさい。
- 内部にキーを保持する配列
#keysと、値を保持する配列#valuesをプライベートフィールドとして持つこと。 set(key, value)メソッドを持ち、指定されたキーと値のペアを追加すること。すでにキーが存在する場合は値を更新すること。get(key)メソッドを持ち、指定されたキーに対応する値を返すこと。キーが存在しない場合はundefinedを返すこと。has(key)メソッドを持ち、キーが存在する場合はtrue、存在しない場合はfalseを返すこと。delete(key)メソッドを持ち、指定されたキーとその値のペアを削除し、削除に成功した場合はtrue、キーが存在しなかった場合はfalseを返すこと。sizeゲッターを持ち、現在登録されているペアの総数を返すこと。
演習2
JavaScriptの標準の例外処理クラスである Error を継承したカスタム例外クラスである ValidationError を作成しなさい。
本クラスは、以下の仕様を満たすように設計しなさい。
- コンストラクタで、エラーメッセージを表す
message、エラーが発生したフィールド名を表すfield、および無効と判断された値を表すinvalidValueを受け取ること。 - 親クラスのコンストラクタを適切に呼び出すこと。
- エラーオブジェクトの名称を表す
nameプロパティの値を"ValidationError"に上書きすること。 - 設定されたフィールド名と無効な値を外部から参照できるよう、
fieldプロパティとinvalidValueプロパティを保持すること。 ValidationErrorのインスタンスに対してconsole.log(error.message)を呼び出した際に、指定したエラーメッセージが出力されることを確認しなさい。
演習3
JavaScriptの標準ライブラリである EventTarget に類似した、イベントの登録と通知を行うイベントエミッタークラスである SimpleEventEmitter を作成しなさい。
本クラスは、以下の仕様を満たすように設計しなさい。
- イベント名とリスナー関数の配列をマッピングして保持するプライベートフィールド
#listenersを持つこと。 addEventListener(event, listener)メソッドを持ち、指定されたイベント名に対してリスナー関数を登録すること。removeEventListener(event, listener)メソッドを持ち、指定されたイベント名から特定のリスナー関数を登録解除すること。dispatchEvent(event, data)メソッドを持ち、指定されたイベント名に登録されているすべてのリスナー関数に対して、引数としてdataを渡して順番に呼び出し実行すること。
演習の解答例
演習1の解答例
プライベート配列を用いてデータを隠蔽した SimpleMap クラスの実装例は以下の通りである。
キーと値を別々のプライベート配列で管理し、キーのインデックスを特定することで、追加・取得・削除・存在確認の各機能を実現している。
また、ゲッターを用いて登録件数を動的に計算して返すようにしている。
simple_map.js
class SimpleMap {
#keys = [];
#values = [];
set(key, value) {
const index = this.#keys.indexOf(key);
if (index !== -1) {
this.#values[index] = value;
} else {
this.#keys.push(key);
this.#values.push(value);
}
}
get(key) {
const index = this.#keys.indexOf(key);
if (index !== -1) {
return this.#values[index];
}
return undefined;
}
has(key) {
return this.#keys.indexOf(key) !== -1;
}
delete(key) {
const index = this.#keys.indexOf(key);
if (index !== -1) {
this.#keys.splice(index, 1);
this.#values.splice(index, 1);
return true;
}
return false;
}
get size() {
return this.#keys.length;
}
}
// 動作確認用
const map = new SimpleMap();
map.set("name", "Alice");
map.set("age", 20);
console.log(map.get("name"));
console.log(map.size);
console.log(map.has("age"));
map.delete("age");
console.log(map.has("age"));
console.log(map.size);
実行結果
Alice
2
true
false
1
演習2の解答例
標準の Error クラスを拡張した ValidationError クラスの実装例は以下の通りである。
super を用いて基底クラスのコンストラクタに必要な情報を引き渡すことで、標準のエラー動作を継承している。
その上で、エラーが発生した箇所や無効なデータを表す独自の情報をプロパティとして追加保持している。
validation_error.js
class ValidationError extends Error {
constructor(message, field, invalidValue) {
super(message);
this.name = "ValidationError";
this.field = field;
this.invalidValue = invalidValue;
}
}
// 動作確認用
try {
throw new ValidationError("メールアドレスの形式が正しくありません", "email", "invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.name);
console.log(error.message);
console.log(error.field);
console.log(error.invalidValue);
}
}
実行結果
ValidationError
メールアドレスの形式が正しくありません
email
invalid-email
演習3の解答例
イベントを登録・発火する SimpleEventEmitter クラスの実装例は以下の通りである。
プライベートなオブジェクトを用いてイベント名ごとにコールバック関数の配列を格納している。
発火時には対象となる配列に登録されたすべての関数を取り出し、引数で渡されたデータを適用して順次実行している。
event_emitter.js
class SimpleEventEmitter {
#listeners = {};
addEventListener(event, listener) {
if (!this.#listeners[event]) {
this.#listeners[event] = [];
}
this.#listeners[event].push(listener);
}
removeEventListener(event, listener) {
if (!this.#listeners[event]) {
return;
}
const index = this.#listeners[event].indexOf(listener);
if (index !== -1) {
this.#listeners[event].splice(index, 1);
}
}
dispatchEvent(event, data) {
if (!this.#listeners[event]) {
return;
}
for (const listener of this.#listeners[event]) {
listener(data);
}
}
}
// 動作確認用
const emitter = new SimpleEventEmitter();
const greet = (data) => {
console.log(`こんにちは、${data.name}さん!`);
};
emitter.addEventListener("greet", greet);
emitter.dispatchEvent("greet", { name: "太郎" });
emitter.removeEventListener("greet", greet);
emitter.dispatchEvent("greet", { name: "太郎" });
実行結果
こんにちは、太郎さん!