前回からのつづき

前回、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」の関係例
      • 「車にはエンジンがある」
      • 「車にはタイヤがある」
      • 「車にはハンドルがある」

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 で移動

注意点

  1. 名前の衝突に注意

同じキーがあると後勝ちになります。

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();
  1. プロトタイプチェーンは使われない

mixin は、プロトタイプに登録されないため、メソッドはインスタンスごとにコピーされます。そのため、メモリ効率は継承より劣りますが、柔軟性が高いという利点があります。

  1. 初期化処理は自分で呼ぶ必要がある

前述の通り、 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)場合
    • 機能を組み合わせたい場合
    • 柔軟で壊れにくい設計にしたい場合