JavaScript

JavaScriptのClass入門:constructor・private・継承・thisを理解する

この記事でわかること

JavaScriptのClassを在庫管理の例で解説。constructor、privateフィールド、getter、static、extends、super、thisと不正な更新を防ぐ設計を学びます。

第6回では、在庫数を管理するクラスを作ります。constructor、インスタンスメソッド、getter、static、privateフィールドを使い、外から在庫を不正な値に書き換えられない構造を学びます。継承による振る舞いの追加と、コールバックでのthisも扱います。

クラスとインスタンスを区別する

クラスは状態と操作をまとめる

クラスは、データとそのデータを扱う操作をまとめて定義する方法です。newで作った各インスタンスはそれぞれの状態を持ちます。JavaScriptのクラスはプロトタイプの仕組みの上に成り立ちます。

inventory-item.mjsを作成する

Node.jsで実行できる次のクラスを保存します。数値は0以上の安全な整数に制限し、減らすときに在庫不足を確認します。

export class InventoryItem {
  #name;
  #quantity;

  constructor(name, quantity = 0) {
    if (typeof name !== 'string' || name.trim() === '') {
      throw new TypeError('商品名が必要です。');
    }
    InventoryItem.validateQuantity(quantity);
    this.#name = name.trim();
    this.#quantity = quantity;
  }

  static validateQuantity(value) {
    if (!Number.isSafeInteger(value) || value < 0) {
      throw new RangeError('数量は0以上の安全な整数にしてください。');
    }
  }

  get name() { return this.#name; }
  get quantity() { return this.#quantity; }

  add(amount) {
    InventoryItem.validateQuantity(amount);
    const next = this.#quantity + amount;
    InventoryItem.validateQuantity(next);
    this.#quantity = next;
  }

  take(amount) {
    InventoryItem.validateQuantity(amount);
    if (amount > this.#quantity) throw new RangeError('在庫が不足しています。');
    this.#quantity -= amount;
  }

  describe() {
    return `${this.name}:在庫${this.quantity}個`;
  }

  toJSON() {
    return { name: this.name, quantity: this.quantity };
  }
}

export class ReorderItem extends InventoryItem {
  #threshold;

  constructor(name, quantity, threshold) {
    super(name, quantity);
    InventoryItem.validateQuantity(threshold);
    this.#threshold = threshold;
  }

  get needsReorder() {
    return this.quantity < this.#threshold;
  }

  describe() {
    return `${super.describe()}${this.needsReorder ? '(補充が必要)' : ''}`;
  }
}

private・getter・staticを読む

privateフィールドで更新経路を限定する

#quantityはクラスの外から直接参照できません。数量を変えるにはaddtakeを使うため、検証を通らない変更を防げます。getterのquantityにはsetterを用意していないので、外側から数量を代入する設計にはしていません。

privateは、アプリ内の操作ルールを守りやすくする仕組みです。ブラウザーへ送った秘密情報を隠せるようになる機能ではありません。

staticはクラス自体に属する

InventoryItem.validateQuantity(...)のように、インスタンスを作らず呼べます。今回の検証関数は特定の商品の状態を使わないため、staticメソッドにしています。

JSON化する値を明示する

privateフィールドは通常の公開プロパティとして列挙されません。toJSONを定義すると、JSON.stringifyへ渡したときに公開するデータを指定できます。JSONからクラスのメソッドまで自動で復元されるわけではありません。

継承とthisの動作を確認する

class-demo.mjsを実行する

同じフォルダーへclass-demo.mjsを保存し、node class-demo.mjsを実行します。

import { InventoryItem, ReorderItem } from './inventory-item.mjs';

const notebook = new InventoryItem('ノート', 5);
notebook.take(2);
notebook.add(1);
console.log(notebook.describe()); // ノート:在庫4個
console.log(JSON.stringify(notebook)); // {"name":"ノート","quantity":4}

const pen = new ReorderItem('ペン', 3, 5);
console.log(pen.describe()); // ペン:在庫3個(補充が必要)
pen.add(5);
console.log(pen.needsReorder); // false
console.log(pen instanceof InventoryItem); // true

const describeLater = () => pen.describe();
console.log(describeLater()); // ペン:在庫8個

try {
  notebook.take(99);
} catch (error) {
  console.log(error.message); // 在庫が不足しています。
}
console.log(notebook.quantity); // 失敗後も4

superで親の初期化と処理を呼ぶ

派生クラスのconstructorでは、thisを使う前にsuper(...)で親を初期化します。super.describe()は親の実装を呼び、補充メッセージだけを追加しています。子から親の#quantityへ直接アクセスせず、公開getterを使います。

メソッドだけを取り出すとthisが変わる

const fn = pen.describe; fn();のように切り離して呼ぶと、この例ではthisが失われて失敗します。() => pen.describe()というアロー関数で呼び出しを包むか、pen.describe.bind(pen)で結び付けます。アロー関数の正しい記法は、上のdescribeLaterを参照してください。

継承だけに頼らない設計

関係が自然か確認する

ReorderItemは在庫商品として同じ操作を提供し、補充判定を追加する例です。機能を再利用したいという理由だけで無関係なクラスを継承すると、親の変更の影響が広がります。

合成という選択肢

通知方法や保存先のように差し替えたい処理は、別のオブジェクトや関数を渡す方法もあります。次回のタスクキューは、タスクの具体的な処理を継承で増やさず、実行する関数を受け取る構成にします。

練習と参考資料

不正な状態にならないか確認する

  • 負数、小数、安全な整数の範囲を超える値を数量として渡します。
  • 在庫不足の操作に失敗しても、元の在庫数が維持されることを確認します。
  • 別の商品インスタンスを作り、数量が混ざらないことを確認します。

最終回の複雑な処理へ進む

クラスで作る並列タスクキューで、private状態・Promise・エラー処理を組み合わせます。

参考資料

y.
WRITTEN BY

y_ymo10

SEの部屋で、JavaScript・TypeScript・React.js・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →