JavaScript アドバンス

JS メタプログラミング

1. メタプログラミングとは

メタプログラミングとは、プログラムが自分自身を操作するための様々な手法を指します。具体的には以下のようなことが可能です。

  • 実行時にオブジェクトを修正する
  • 実行時にオブジェクトを検査(インスペクション)する
  • 実行時にオブジェクトを制御する
  • 実行中のオペレーションをインターセプト(遮断・割り込み)する
  • 関数やクラスを修正する
  • 動的にコードを生成する

1.1 わかりやすい解説

通常、コードはデータを扱います。
メタプログラミングでは、コードがコードを扱います。

2. オブジェクトのインスペクション

Object.keys() メソッドを使用すると、オブジェクトのプロパティを検査(インスペクション)できます。
Object.keys() を使うことは、メタプログラミングの最もシンプルな例の一つです。

実装例:
このコードは、別のコード(オブジェクト)を分析しています。

// オブジェクトを作成
const user = {name: "Jan", age: 40};

// オブジェクトのキーを配列に格納
const myArr = Object.keys(user);

3. オブジェクトの変更

典型的なメタプログラミングのタスクは、オブジェクトの振る舞いを修正することです。

実装例:

// オブジェクトを作成
const person = {name: "John", age: 41};

// "name" プロパティが常に "secret" を返すように定義
Object.defineProperty(person, "name", {
  get() { return "secret"; }
});

let name = person.name; // "secret" が返される

4. 動的コードの生成

メタプログラミングには、動的なコード生成も含まれます。
JavaScript は実行時に関数を生成することができます。

実装例:

const fn = new Function("a", "b", "return a + b");

5. メタプログラミングの活用例

コンセプト説明
バリデーションプロパティに設定できる値を制限する
ロギングProxy を使用してプロパティの変更を表示する
デバッグProxy を使用してオペレーションをインターセプトする
フレームワークVue、MobX、Svelte などが状態変化を検知するために使用
ORM / データベースマッピングオブジェクトをラップし、DBスキーマに基づいてフィールドを作成する
動的 API実行時に関数やオブジェクト構造を作成する

6. Proxy によるメタプログラミング

JavaScript における ProxyReflect という2つのオブジェクトは、メタレベルでのプログラミングを可能にします。

Proxy を使用すると、値の読み取りや書き込みといったプロパティ操作をインターセプトできます。
以下の例では:

  1. user オブジェクトが Proxy でラップされています。
  2. Proxy は set() トラップを使用して、プロパティが設定されるたびにログを出力します。

実装例(プロパティ値の変更をログ出力):

// オブジェクトを作成
const user = {name: "Jan", age: 40};

// オブジェクトを Proxy でラップ
const proxy = new Proxy(user, {
  // set トラップを使用
  set(target, property, value) {
    // 変更をログ出力
    log(property + "; " + value);
    return target[property] = value;
  }
});

// プロパティを変更
proxy.name = "John";
proxy.age = 45;
proxy.name = "Paul";

7. Proxy と Reflect の併用

Reflect を使用することで、Proxy の動作を通常のオブジェクトの動作と一致させ、より安全に処理を委譲できます。

以下の例では:

  • user オブジェクトが Proxy でラップされています。
  • Proxy は set() トラップを使用して、プロパティ設定時にログを出力します。
  • set トラップ内で Reflect.set() を使用し、本来の動作を安全に転送(フォワーディング)しています。

実装例:

// オブジェクトを作成
const user = {name: "Jan", age: 40};

// オブジェクトを Proxy でラップ
const proxy = new Proxy(user, {
  // set トラップを使用
  set(target, property, value) {
    // 変更をログ出力
    log(property + ": " + value);
    // Reflect を使用して安全に本来の処理へ転送
    return Reflect.set(target, property, value);
  }
});