前回からのつづき
前回、class の基本的な定義の仕方について説明してきました。今回もその続きから説明していきたいと思います。
インスタンス生成
JavaScript のクラスは関数オブジェクトなので、そのまま利用することもできます。ただし、通常は static メソッドを使う場合に限られ、インスタンスを生成せずにクラスを直接使うケースは多くありません。
基本的には、クラスをもとに個別のオブジェクト(インスタンス)を生成して利用します。
インスタンスの作成には、 new を使います。その際、constructor が実行されます。
基本
class User {
constructor(name) {
this.name = name;
}
}
const u = new User("Kon");
new の役割
new を使うと、次の処理が自動で行われます。
- 新しいオブジェクト(インスタンス)を作る。
- そのオブジェクト(インスタンス)を
thisに束縛する。 constructorを実行する。- プロトタイプチェーンを設定する。
setter / getter メソッド
setter / getter は、プロパティの読み書きに処理を挟みたいときに使う構文です。
見た目はプロパティですが、内部ではメソッドとして動作します。
例えば、
privateのプロパティの実際の形を隠蔽することで、後々、変更可能にしたい場合- 出力時のフォーマットを変更したい場合
- 書き込み時に制限を設けたい場合
などにも利用できます。
基本構文
class User {
#age = 0;
get age() {
return `${this.#age}才`; // ~才
}
set age(value) {
if (value < 0) throw new Error("年齢は0以上です"); // マイナスを設定するとエラー
this.#age = value;
}
}
const u = new User();
u.age = 20; // setter が呼ばれる。 setter のパラメータとして 20 が渡される。
console.log(u.age); // getter が呼ばれる
特徴
getは「値を読むとき」に呼ばれる。getは、「getプロパティ名() 」で記述する。setは「値を書き込むとき」に呼ばれる。setは、「setプロパティ名(value) 」で記述する。- 関数呼び出しではなく、プロパティアクセスのように使える。
なぜ使うのか
- バリデーションチェック(不正な値の書き込みを防ぐ)
- 内部状態の隠蔽(private と相性が良い)
- 外部 API をシンプルに保てる
- 後から内部実装を変えても外部コードを壊さない
👉 getter / setter は「プロパティのように見えるメソッド」です。内部状態を安全に扱いたいときに非常に便利です。
注意点
getter / setter を使うと…
- 読むだけで処理が走る。
- 書き込むだけで処理が走る。
- 何が起きるかコードを見ないとわからない。
- デバッグが難しくなる。
という “予測しづらい動き” が混ざります。そのため、「ただのデータを入れておきたいだけ」という純粋なデータ構造の用途には向きません。
又、プロパティに比べ、関数アクセスがオーバーヘッドとなり、大量のデータ処理には向かないです。
クラス継承( extends )
JavaScript のクラスは、extends を使って 既存のクラスを拡張できます。
継承は「A は B の一種である(is-a)」というクラス間の関係を表すときに使います。
例えば、「犬は動物の一種である」という関係がある場合、犬クラスを定義する際、既に存在する動物クラスを継承して定義することが可能です。
基本構文
class Animal { // 動物クラス
constructor(name) {
this.name = name;
}
eat() {
console.log("食べる");
}
speak() {
console.log("...");
}
}
class Dog extends Animal { // 犬クラス(動物クラスを継承)
constructor(name) {
super(name); // 親の constructor を呼ぶ
}
speak() {
console.log("ワン!");
}
}
ポイント
- 継承を利用する場合は、
extendsで親クラスを指定する。 - 子クラスは親クラスのメソッドをそのまま使える。
- 同名メソッドを定義すると「オーバーライド」になる。
super()で親のconstructorを呼び出せる。
継承の注意点
継承は便利ですが、構造が複雑になりやすいため、JavaScript では「必要最小限」にとどめるのが一般的です。
特に、
- 親クラスの変更が子クラスに波及しやすい。
- 階層が深くなるとデバッグが難しい。
といった理由から、実務では composition(合成) が好まれます。
composition(合成)
composition(合成)は、継承とは異なり、複数の機能を組み合わせてオブジェクトを作る考え方です。
JavaScript は動的言語であり、オブジェクトの合成が自然にできるため、継承よりも composition が推奨される場面が多くあります。
composition が好まれる理由
- JavaScript は動的で、オブジェクトの合成が簡単
- 多重継承ができない。
- 継承は密結合になりやすく壊れやすい。
- 実務では「is-a」より「has-a」の関係が多い。
- 「is-a」関係例
- 「犬は動物の一種である」
- 「has-a」の関係例
- 「車にはエンジンがある」
- 「車にはタイヤがある」
- 「車にはハンドルがある」
- 「is-a」関係例
JavaScript では、Object.assign を使い、プロパティのコピーを行うことで、composition(合成)が実現できます。
下記に、クラスのインスタンスを合成するパターンを示します。
class Engine {
silent() { console.log("ブォーン"); }
}
class Tire {
rolling() { console.log("前進"); }
}
class Handle {
right() { console.log("右"); }
left() { console.log("左"); }
}
class Car {
constructor() {
Object.assign(this,
new Engine(),
new Tire(),
new Handle()
);
}
}
const my_car = new Car();
my_car.silent(); // ブォーン
my_car.rolling(); // 前進
my_car.right(); // 右
my_car.left(); // 左
特徴
- 必要な機能だけを柔軟に組み合わせられる
- 継承よりも疎結合で壊れにくい
- 多重継承の代替として使える
👉 composition は、JavaScript の「動的にプロパティを追加できる」という性質と非常に相性が良い設計手法です。
composition(合成)の一般的な書き方
composition(合成)は、上記の形でも実現可能ですが、通常は、部品となるオブジェクトの再利用性を高める為に、 class ではなく、下記の様に mixin オブジェクトにすることが一般的です。
const engine = { // mixin オブジェクト
silent() { console.log("ブォーン"); }
};
const tire = { // mixin オブジェクト
rolling() { console.log("前進"); }
};
const handle = { // mixin オブジェクト
right() { console.log("右"); }
left() { console.log("左"); }
};
class Car {
constructor() {
Object.assign(this, engine, tire, handle);
}
}
const my_car = new Car();
my_car.silent();
my_car.rolling();
my_car.right();
my_car.left();
mixin とは
「機能(メソッドの集合)をオブジェクトとして切り出し、必要なクラスに合成して使う技法」 のことです。
つまり:
- 継承しない
- クラス階層を作らない
- 必要な機能だけを “混ぜる(mix in)”
という、とても柔軟な設計方法です。
👉 その際に利用する機能(メソッドの集合)をオブジェクトを mixin オブジェクトと呼びます。
mixin オブジェクトの初期化
mixin オブジェクトは、 class と異なり、「ただのオブジェクト」なので constructor を持てません。
その為、初期化が必要な場合は、初期化関数を用意する のが一般的です。
const movable = {
initMovable(speed) {
this.speed = speed;
},
move() {
console.log(`${this.speed} km/h で移動`);
}
};
class Car {
constructor() {
Object.assign(this, movable);
this.initMovable(60);
}
}
const c = new Car();
c.move(); // 60 km/h で移動
注意点
- 名前の衝突に注意
同じキーがあると後勝ちになります。
const engine = {
silent() { console.log("ブォーン"); }
};
const tire = {
silent() { console.log("ゴロゴロ"); }
};
const handle = {
right() { console.log("右"); }
left() { console.log("左"); }
};
class Car {
constructor() {
Object.assign(this, engine, tire, handle);
}
}
const my_car = new Car();
my_car.silent(); // ゴロゴロ 後から定義された方に上書きされる
my_car.right();
my_car.left();
- プロトタイプチェーンは使われない
mixinは、プロトタイプに登録されないため、メソッドはインスタンスごとにコピーされます。そのため、メモリ効率は継承より劣りますが、柔軟性が高いという利点があります。
- 初期化処理は自分で呼ぶ必要がある
前述の通り、
mixinオブジェクトには、constructorがないため、初期化関数を用意して呼び出す必要があります。
クラスとモジュールの組み合わせ
クラスはモジュールと組み合わせることで、「役割が明確な部品」として扱いやすくなります。
実務では、小さなクラスを 1 ファイルにまとめて export するパターンがよく使われます。
例:ローカルストレージのラッパー
storage.js
export class Storage {
#prefix = "app_";
set(key, value) {
localStorage.setItem(this.#prefix + key, JSON.stringify(value));
}
get(key) {
return JSON.parse(localStorage.getItem(this.#prefix + key));
}
}
main.js
import { Storage } from "./storage.js";
const store = new Storage();
store.set("user", { name: "Kon" });
console.log(store.get("user"));
この構成が強い理由
- 小さくて理解しやすい。
- テストしやすい。
- 再利用しやすい。
- モジュールとして自然に扱える。
まとめ
constructorはインスタンスの初期化処理を書く場所- メソッドはプロトタイプに登録される。
privateフィールドはインスタンス毎の内部状態staticフィールドはクラス共通の状態- クラスはモジュールと組み合わせると強力
- 小さなクラスを
exportするのが実務で最も使われるパターン - 継承を使うべきとき
- A は B の一種である(is-a)場合
- 明確な階層構造がある場合
- 親クラスの振る舞いをそのまま使いたい場合
- composition を使うべきとき
- A は B を持っている(has-a)場合
- 機能を組み合わせたい場合
- 柔軟で壊れにくい設計にしたい場合