JavaScript アドバンス

JavaScript Proxy

1. プロキシ(Proxy)とは?

プロキシ(Proxy)は、他のオブジェクトをラップ(包む)することができるJavaScriptオブジェクトです。 プロキシを使用することで、ラップされたオブジェクトに対する操作をコントロールできるようになります。

プロキシは、誰かが以下のような操作を行った際に、コードをトラップしてインターセプト(割り込み)することができます。

  • プロパティの読み取り(get)
  • プロパティの設定(set)
  • プロパティの削除(deleteProperty)
  • プロパティの存在確認(has)
  • 関数の呼び出し(apply)
  • オブジェクトのインスタンス化(construct)

プロキシを使えば、オブジェクトとのインタラクションが発生した時に独自のコードを実行させることができます。いわば、あなたのコードとJavaScriptオブジェクトの間の**仲介役(ミドルマン)**として機能します。

2. プロキシの構文

const proxy = new Proxy(target, handler);
  • target - 元のオブジェクトまたは関数
  • handler - トラップメソッドを含むオブジェクト

2.1 実装例

const myObject = {name: "Jan"};

const proxy = new Proxy(myObject, {
  get(target, prop) {
    return target[prop];
  }
});

3. プロキシによるロギング

典型的なプロキシの活用例は、オブジェクトの変更をロギングすることです。
以下のデモでは、以下の処理を行っています。

  • オブジェクトをプロキシでラップする
  • プロパティが読み取られたり書き込まれたりするたびにログを出力する
  • すべての getset 操作をリアルタイムで記録する

3.1 実装例:すべてのプロパティ値の変更をログ出力

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

// プロキシを作成
const proxy = new Proxy(user, {
  get(target, property) {
    log("取得中(Getting): " + property);
    return target[property];
  },
  set(target, property, value) {
    log("設定中(Setting): " + property);
    target[property] = value;
    return true; // 成功を示す
  }
});

proxy.name = "John";
proxy.age = 42;

let text1 = proxy.name;
let text2 = proxy.age;

4. Reflect を併用したプロキシ(最も一般的)

以下のデモでは、Proxy ハンドラー内で Reflect.get()Reflect.set() を使用しています。

  • get トラップ内で Reflect.get(target, property, receiver) を使用
  • set トラップ内で Reflect.set(target, property, value, receiver) を使用 Reflect を使用することで、プロキシの挙動を通常のオブジェクトの挙動と正確に一致させることができます。

4.1 実装例:Reflect による安全な転送

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

// プロキシを作成
const proxy = new Proxy(user, {
  get(target, property) {
    log("取得中(Getting): " + property);
    // 安全な転送(Safe Forwarding)
    return Reflect.get(target, property);
  },
  set(target, property, value) {
    log("設定中(Setting): " + property);
    // 安全な転送(Safe Forwarding)
    return Reflect.set(target, property, value);
  }
});

proxy.name = "John";
proxy.age = 42;

let text1 = proxy.name;
let text2 = proxy.age;

4.2 Proxy + Reflect のフロー

このフローは、Proxy と Reflect を組み合わせた JavaScript メタプログラミングの本質です。

フローの解説:

  1. あなたのコードがオブジェクトを操作する。
  2. Proxy がその操作をインターセプトする。
  3. トラップ内のコードが、何を行うべきか決定する。
  4. Reflect がその操作を安全に転送(フォワード)する。
  5. ターゲットオブジェクトが実際の処理を受け取る。

5. なぜプロキシを使うのか?

プロキシを利用することで、以下のような機能を実現できます。

  • ロギングの追加
  • 値の変更に対するバリデーション(検証)
  • プロパティの自動生成
  • 機密データの保護
  • 仮想プロパティや算出プロパティの作成
  • 関数呼び出しのインターセプト
  • リアクティブなシステムの構築(Vue.js など)

5.1 プロキシによるバリデーション

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

// プロキシを作成
const proxy = new Proxy(user, {
  set(target, prop, value) {
    if (prop === "age" && value < 0) {
      const text = "年齢を負の数にすることはできません!";
      document.getElementById("demo").innerHTML = text;
      return false; // 更新を拒否
    }
    return Reflect.set(target, prop, value);
  }
});

proxy.age = 45; // OK
proxy.age = -5; // エラー表示

5.2 仮想プロパティ(Virtual Properties)

// オブジェクトを作成
const person = {
  first: "John",
  last: "Doe"
};

// プロキシを作成
const virtual = new Proxy(person, {
  get(target, prop) {
    if (prop === "fullName") {
      return target.first + " " + target.last;
    }
    return Reflect.get(target, prop);
  }
});

let text = virtual.fullName; // "John Doe"

5.3 動的関数(Dynamic Functions)

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

let result = fn(3, 2);

6. メタプログラミング(Metaprogramming)

メタプログラミングにより、JavaScript は以下のことが可能になります。

  • 挙動のインターセプト
  • 挙動の修正
  • 新しい挙動の定義
  • 挙動の動的な生成 これにより、開発者は言語の内部動作を深くコントロールできるようになります。

7. プロキシのトラップ(Proxy Traps)

トラップ(Trap)とは、Proxy ハンドラー内の関数のことです。
Proxy に対して特定の操作が行われた際に実行されます。
以下は、JavaScript のすべてのトラップ、トリガー条件、パラメーター、および期待される戻り値の完全な解説です。

トラップ名トリガー条件
getプロパティが読み取られた時
setプロパティが変更された時
hasin 演算子が使用された時
deletePropertyプロパティが削除された時
apply関数が呼び出された時
constructnew でオブジェクトが作成された時
getOwnPropertyDescriptorプロパティディスクリプタが取得された時
definePropertyプロパティが定義された時
getPrototypeOfプロトタイプが取得された時
setPrototypeOfプロトタイプが設定された時
isExtensible拡張可能かチェックされた時
preventExtensions拡張禁止に設定された時
ownKeysプロパティがリストアップされた時

上記は 2025 年時点(2026 年現在も最新)の正確なリストであり、ECMAScript で定義されている 13 種類すべての Proxy トラップを含んでいます。

7.1 handler.get()

プロパティが読み取られた時にトリガーされます。

get(obj, prop, receiver) {
  return Reflect.get(obj, prop, receiver);
}
  • トリガー: proxy.property, proxy["property"], object.property()
  • パラメーター: obj(ターゲット), prop(アクセスされたプロパティ), receiver(通常は proxy 自体)
  • 戻り値: プロパティの値

7.2 handler.set()

プロパティが変更された時にトリガーされます。

set(obj, prop, value, receiver) {
  return Reflect.set(obj, prop, value, receiver);
}
  • トリガー: proxy.property = value, proxy["property"] = value
  • パラメーター: obj, prop, value(新しい値), receiver
  • 戻り値: 成功なら true、失敗なら false

7.3 handler.has()

in 演算子をインターセプトします。

has(obj, prop) {
  return Reflect.has(obj, prop);
}
  • トリガー: "property" in proxy
  • パラメーター: obj, prop
  • 戻り値: true または false

7.4 handler.deleteProperty()

delete 演算子をインターセプトします。

deleteProperty(obj, prop) {
  return Reflect.deleteProperty(obj, prop);
}
  • トリガー: delete proxy.property
  • パラメーター: obj, prop
  • 戻り値: 成功なら true、失敗なら false

7.5 handler.apply()

関数が呼び出された時にトリガーされます。

apply(func, thisArg, args) {
  return Reflect.apply(func, thisArg, args);
}
  • トリガー: proxy(), proxy.call(), proxy.apply()
  • パラメーター: func(呼び出し可能なオブジェクト), thisArg, args(引数配列)
  • 戻り値: 関数の戻り値

7.6 handler.construct()

new 演算子をインターセプトします。

construct(obj, args, newTarget) {
  return Reflect.construct(obj, args, newTarget);
}
  • トリガー: new proxy()
  • パラメーター: obj(コンストラクタ), args, newTarget
  • 戻り値: 新しいインスタンス(オブジェクト)

construct トラップは new を使用した場合にのみ実行されます。

Object.create() やクラス宣言だけでは実行されません。

7.7 handler.getOwnPropertyDescriptor()

プロパティディスクリプタの取得をインターセプトします。

getOwnPropertyDescriptor(obj, prop) {
  return Reflect.getOwnPropertyDescriptor(obj, prop);
}
  • トリガー: Object.getOwnPropertyDescriptor(obj, prop)
  • 戻り値: ディスクリプタオブジェクト、または undefined

7.8 handler.defineProperty()

Object.defineProperty() をインターセプトします。

defineProperty(obj, prop, descriptor) {
  return Reflect.defineProperty(obj, prop, descriptor);
}
  • トリガー: Object.defineProperty()
  • 戻り値: 成功なら true、失敗なら false

7.9 handler.getPrototypeOf()

プロトタイプの検索をインターセプトします。

getPrototypeOf(obj) {
  return Reflect.getPrototypeOf(obj);
}
  • トリガー: Object.getPrototypeOf(), __proto__ へのアクセスなど
  • 戻り値: オブジェクト、または null

7.10 handler.setPrototypeOf()

プロトタイプの設定をインターセプトします。

setPrototypeOf(obj, prototype) {
  return Reflect.setPrototypeOf(obj, prototype);
}
  • トリガー: Object.setPrototypeOf()
  • 戻り値: 成功なら true、失敗なら false

7.11 handler.isExtensible()

オブジェクトが拡張可能かどうかのチェックをインターセプトします。

isExtensible(obj) {
  return Reflect.isExtensible(obj);
}
  • トリガー: Object.isExtensible()
  • 戻り値: true または false

7.12 handler.preventExtensions()

オブジェクトを拡張禁止にする操作をインターセプトします。

preventExtensions(obj) {
  return Reflect.preventExtensions(obj);
}
  • トリガー: Object.preventExtensions()
  • 戻り値: 成功なら true、失敗なら false

7.13 handler.ownKeys()

プロパティ名やシンボルをリストアップする操作をインターセプトします。

ownKeys(obj) {
  return Reflect.ownKeys(obj);
}
  • トリガー: Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols()
  • 戻り値: 重複のないキーの配列

8. プロキシトラップと Reflect メソッドの対応

プロキシの各トラップは、JavaScript の内部操作(Internal Operations)に対応しています。

  • [[Construct]]
  • [[Call]]
  • [[Get]]
  • [[Set]]
  • [[HasProperty]]
  • [[Delete]]
  • [[DefineProperty]]
  • [[GetOwnProperty]]
  • [[OwnPropertyKeys]]
  • [[GetPrototypeOf]]
  • [[SetPrototypeOf]]
  • [[PreventExtensions]]
  • [[IsExtensible]]

これらは、JavaScript エンジン内部でオブジェクトにアクセスしたり変更したりする際に使用される操作です。Proxy がこれらの操作をインターセプトした際、それらを正しく転送する必要があります。

get(target, property, receiver) {
  return Reflect.get(target, property, receiver);
}

Reflect が使用される理由は、Reflect のメソッドがこれら内部操作と 1対1でミラーリング(鏡写し)されているからです。

  • 正しい戻り値(true/false/ディスクリプタ)を生成します。
  • プロキシのルール(不変条件)を壊すようなエラーの発生を回避します。
  • プロキシを、通常の JavaScript オブジェクトのように(意図的に変更しない限り)振る舞わせることができます。