Featured image of post JavaScript ProxyとReflect API:メタプログラミングパターン Featured image of post JavaScript ProxyとReflect API:メタプログラミングパターン

JavaScript ProxyとReflect API:メタプログラミングパターン

JavaScriptのProxyとReflect APIを活用したメタプログラミング手法を解説。13のトラップ、Reflectメソッド、検証パターン、仮想プロパティ、そしてVue 3やMobXでの実践的な使用例を紹介します。

JavaScriptのProxyReflect APIは、言語が提供するメタプログラミングツールの中でも最も強力なものの一つです。プロパティアクセス、代入、列挙、関数呼び出し、そしてコンストラクタ呼び出しに至るまで、オブジェクトの基本操作をインターセプトしてカスタマイズできます。本記事では、13のProxyトラップ、Reflect API、そしてVue 3やMobX、Immerで使われている実践的なパターンを解説します。

Proxyパターンの基礎

Proxyはターゲットオブジェクトをラップし、ハンドラで定義したトラップ関数を通じて操作をインターセプトします:

const target = { name: "Alice", age: 30 };
const handler = {
  get(target, prop, receiver) {
    console.log(`Getting property "${prop}"`);
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // "Getting property 'name'" → "Alice"

ターゲット自体は変更されず、ハンドラがプロキシ経由の操作を転送・変更・ブロックします。Proxyはラッパー関数やデコレータと異なり、indeletefor...innewといった言語レベルでの操作もカスタマイズできます。


13のProxyトラップ

Proxyは13のトラップを提供し、すべての基本オブジェクト操作をカバーします。最もよく使われるのはgetsetです。以下が完全なリファレンスです:

トラップ発火タイミング引数
getプロパティ読み取りtarget, prop, receiver
setプロパティ書き込みtarget, prop, value, receiver
hasin演算子target, prop
deletePropertydelete演算子target, prop
ownKeysObject.keys()などtarget
apply関数呼び出しtarget, thisArg, args
constructnewキーワードtarget, args, newTarget
getPrototypeOfObject.getPrototypeOftarget
setPrototypeOfObject.setPrototypeOftarget, proto
isExtensibleObject.isExtensibletarget
preventExtensionsObject.preventExtensionstarget
getOwnPropertyDescriptorObject.getOwnPropertyDescriptortarget, prop
definePropertyObject.definePropertytarget, prop, desc

各トラップは不変条件を守る必要があります。例えば、書き込み不可のプロパティに対してgetトラップが異なる値を返してはいけません。


Reflect API

Reflectメソッドは各Proxyトラップに対応し、デフォルトの転送動作を提供します。トラップ内でReflectを使うことで、receiverの正しい伝播が保証され、継承プロパティやgetterのthisバインディングが維持されます:

const parent = {
  get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"

常にReflect経由でトラップを転送し、デフォルト動作を維持しながらカスタムロジックを追加するのが推奨パターンです。


バリデーションとデータ整合性

setトラップでスキーマ制約を強制できます:

const validator = {
  set(target, prop, value) {
    if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
      throw new TypeError("年齢は正の整数である必要があります");
    }
    if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
      throw new TypeError("メールアドレスの形式が不正です");
    }
    return Reflect.set(target, prop, value);
  }
};
const user = new Proxy({}, validator);
user.age = 25;  // OK

読み取り専用Proxyはすべての変更トラップをブロックします:

const readOnly = target =>
  new Proxy(target, {
    set: () => { throw new Error("読み取り専用です"); },
    deleteProperty: () => { throw new Error("読み取り専用です"); },
    defineProperty: () => { throw new Error("読み取り専用です"); },
  });

仮想プロパティと算出値

Proxyを使うと、ターゲットに存在しないプロパティをgetトラップで動的に計算できます:

const range = (from, to) =>
  new Proxy({ from, to }, {
    get(target, prop) {
      if (prop === "size") return target.to - target.from + 1;
      if (prop === "contains") return (v) => v >= target.from && v <= target.to;
      return Reflect.get(target, prop);
    }
  });

const r = range(1, 10);
console.log(r.size);      // 10
console.log(r.contains(5)); // true

WeakMapを使ったキャッシュにより、再計算を防止する遅延評価パターンも実装できます。


プロパティ監視と変更追跡

setトラップを使った簡易Observable:

function observe(target, onChange) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      onChange(prop, value);
      return result;
    }
  });
}

深い監視には再帰的ProxyラッピングとWeakMapキャッシュを組み合わせます。この技法はVue 3のreactive()システムの基盤であり、Vue 2のObject.definePropertyベースのアプローチを置き換えました。


取り消し可能Proxy

Proxy.revocableは、恒久的に無効化できるプロキシを作成します:

const { proxy, revoke } = Proxy.revocable(target, handler);
// 通常通り使用...
revoke();
// 以降の操作は TypeError をスロー

機密リソースへの一時的なアクセス権付与や、クリーンアップ契約の強制に有用です。


プロダクションパターン:Vue 3、MobX、Immer

Vue 3reactive()でProxyを使用し、getトラップで依存関係を追跡し、setで再レンダリングをトリガーします。**MobX 6+**もobservable()にProxyを採用し、Immer.jsはコピーオンライトセマンティクスで不変ステートドラフトを生成します。


パフォーマンス考慮

各トラップ操作にはオーバーヘッドがあります。ホットパスのオブジェクトをタイトループ内でProxyするのは避け、「内側ではなく外側で」選択的に適用します。Proxyはバリデーション、リアクティビティ、仮想リソースに最適ですが、パフォーマンスが重要な処理ではよりシンプルなパターンで十分かを検討しましょう。