JavaScriptのProxyとReflect 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はラッパー関数やデコレータと異なり、in、delete、for...in、newといった言語レベルでの操作もカスタマイズできます。
13のProxyトラップ
Proxyは13のトラップを提供し、すべての基本オブジェクト操作をカバーします。最もよく使われるのはgetとsetです。以下が完全なリファレンスです:
| トラップ | 発火タイミング | 引数 |
|---|---|---|
get | プロパティ読み取り | target, prop, receiver |
set | プロパティ書き込み | target, prop, value, receiver |
has | in演算子 | target, prop |
deleteProperty | delete演算子 | target, prop |
ownKeys | Object.keys()など | target |
apply | 関数呼び出し | target, thisArg, args |
construct | newキーワード | target, args, newTarget |
getPrototypeOf | Object.getPrototypeOf | target |
setPrototypeOf | Object.setPrototypeOf | target, proto |
isExtensible | Object.isExtensible | target |
preventExtensions | Object.preventExtensions | target |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor | target, prop |
defineProperty | Object.defineProperty | target, 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 3はreactive()でProxyを使用し、getトラップで依存関係を追跡し、setで再レンダリングをトリガーします。**MobX 6+**もobservable()にProxyを採用し、Immer.jsはコピーオンライトセマンティクスで不変ステートドラフトを生成します。
パフォーマンス考慮
各トラップ操作にはオーバーヘッドがあります。ホットパスのオブジェクトをタイトループ内でProxyするのは避け、「内側ではなく外側で」選択的に適用します。Proxyはバリデーション、リアクティビティ、仮想リソースに最適ですが、パフォーマンスが重要な処理ではよりシンプルなパターンで十分かを検討しましょう。
