Kodokon kodokon.com

Proxy、Reflect、そしてメタプログラミング

Proxy のトラップで言語の基本的な操作をインターセプトし、Reflect でデフォルトの動作をきれいに委譲します。

10 分 · 3 問

このレッスンを Kodokon で開く

Proxy はターゲットオブジェクトをラップし、13 個のトラップを通じて言語の内部操作をインターセプトします。getsethasin 演算子)、deletePropertyownKeysapplyconstruct などです。Reflect オブジェクトは、まさに同じ操作を関数として、トラップと同じシグネチャで公開します。これは Vue 3 のリアクティビティ、動的なモック、そして ORM 風の API の基盤です。

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
Reflect に委譲した最小限の get トラップ。

なぜ obj[prop] ではなく Reflect.get(obj, prop, receiver) なのでしょうか?それはレシーバ(receiver)のためです。プロパティがアクセサ(ゲッター)である場合、Reflect.getthis をレシーバ(プロキシ、あるいはそれを継承したオブジェクト)に向けて呼び出しますが、obj[prop]this を生のターゲットに設定してしまいます。レシーバがなければ、this を読むゲッターは、オブジェクトがプロキシを継承した途端に壊れてしまいます。

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" thanks to the receiver
レシーバは継承されたゲッターにおいて this を保ちます。

本格的なユースケースはこうです。書き込み時のバリデーション、その場で計算される仮想プロパティ、ロギング、アクセス制御(hasownKeys を通じて _ で始まるキーを隠す)、あるいはデフォルトの動作を持つオブジェクト。なお、Reflect.set は strict モードで例外を投げる代わりに真偽値を返します。あなたのトラップはその戻り値を伝播させなければなりません。

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
書き込み時の透過的なバリデーション。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. get トラップの中で、なぜ target[prop] より Reflect.get(target, prop, receiver) を選ぶのですか?
    • Reflect はプロトタイプをバイパスするので速いから
    • ゲッターが生のターゲットではなく正しい this(レシーバ)で実行されるようにするため
    • target[prop] はトラップを再び発火させ、無限再帰を引き起こすから
    • この 2 つの形式は厳密に等価だから
  2. "id" in obj をインターセプトするトラップはどれですか?
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. 再構成不可・書き込み不可として宣言され、値が 1 のターゲットプロパティに対して、get トラップが 42 を返します。何が起こりますか?
    • プロキシは 42 を返す。トラップは常に優先されるから
    • エンジンは静かに 1 を返す
    • エンジンは不変条件違反として TypeError を投げる
    • 動作は JavaScript エンジンに依存する