JavaScriptのProxyオブジェクトを使うと、別のオブジェクトのプロパティの取得、設定、列挙、関数呼び出しなどを再定義できる。

使いどころ

例えば以下のようなケースに活躍する。

  • プロパティへのアクセスログ。
  • プロパティに設定される値のバリデーション。
  • プロパティに設定される値のフォーマット。
  • プロパティに設定される値のサニタイズ。
  • プロパティの保護。
  • などなど。

わかるような、わからないような使いどころである。外付けできるsettergetterのすごいやつ、のようなイメージだろうか。

使い方

Proxyコンストラクタは、以下の2つの引数を受け取る。

引数 概要
target 処理をカスタマイズしたいオブジェクト。
handler カスタマイズした処理(=関数群)を保持するオブジェクト。

handlerに空のオブジェクトを渡してもProxyオブジェクトは作成される。ただし処理はインターセプトされず、元のオブジェクトの処理がそのまま実行される。

js
const target = { a: 1, b: 2 };
const handler = {}; // 空のハンドラオブジェクト

const proxy = new Proxy(target, handler);

console.log(proxy.a); // 1
console.log(proxy === target); // false

handlerオブジェクトについて

handlerオブジェクトが保持する関数群は、オリジナルのオブジェクトの処理をつかまえることから、トラップと呼ばれる。

トラップには、オブジェクトの内部メソッドに対応する名前が予めつけられている。これらの関数をhandlerオブジェクトのプロパティとして保持し、Proxyコンストラクタに渡すことで、オリジナルの処理を再定義するわけである。

内部メソッド 対応するトラップ
[[GetPrototypeOf]] getPrototypeOf(target)
[[SetPrototypeOf]] setPrototypeOf(target, prototype)
[[IsExtensible]] isExtensible(target)
[[PreventExtensions]] preventExtensions(target)
[[GetOwnProperty]] getOwnPropertyDescriptor(target, propertyKey)
[[DefineOwnProperty]] defineProperty(target, propertyKey, attributes)
[[HasProperty]] has(target, propertyKey)
[[Get]] “get(target, propertyKey, receiver?)`
[[Set]] set(target, propertyKey, value, receiver?)
[[Delete]] deleteProperty(target, propertyKey)
[[OwnPropertyKeys]] ownKeys(target)

また、関数オブジェクトの場合は以下のようなトラップも使える。

内部メソッド 対応するトラップ
[[Call]] apply(target, thisArgument, argumentsList)
[[Construct]] construct(target, argumentsList, newTarget)

オリジナルのオブジェクトが返す内部メソッドの値は、これらの関数が返す値によって上書きされる。

なお、トラップ内でオリジナルのオブジェクトに何らかの操作を行う場合、Reflectオブジェクトのメソッドを通じて行うのが一般的である。

Reflectオブジェクトとは、オブジェクトを一貫したインターフェースで操作するための静的なグローバルオブジェクトだ(Reflectオブジェクトの詳細は別ページにまとめた)。

操作対象のオブジェクトを直接操作しても結果は変わらないが、Reflectオブジェクトの方が記法が一貫していてわかりやすい。Reflectオブジェクト自体、handlerのトラップ内で、オリジナルのオブジェクトを操作することを主目的に用意されたようだ(そんなことがMDNに書いてあった)。

例えばgetterを上書きする場合、以下のように書く。

js
const target = {
  notProxied: "私は人間です。",
  proxied: "私も人間です。",
};

const handler = {
  get(target, prop, receiver) {
    if (prop === "proxied") {
      return "私は人間ですか?";
    }
    return Reflect.get(...arguments);
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.notProxied); // "私は人間です"
console.log(proxy.proxied); // "私は人間ですか?"

参考元:Proxy() constructor – JavaScript | MDN

Proxyの格好いい使い方

VanJSという超軽量のUIフレームワークでは、Proxyオブジェクトが効果的に使われている。

js
let tag = (ns, name, ...args) => {
  console.log(`Namespace: ${ns}, Name: ${name}, Arguments:${args}`);
};

let handler = (ns) => ({ get: (_, name) => tag.bind(undefined, ns, name) });

let tags = new Proxy((ns) => new Proxy(tag, handler(ns)), handler());

let { div, span } = tags;
let { svg } = tags("http://www.w3.org/2000/svg");
div({ class: "className" }, "text"); // Namespace: undefined, Name: div, Arguments:[object Object],text
span(); // Namespace: undefined, Name: span, Arguments:
svg(); // Namespace: http://www.w3.org/2000/svg, Name: svg, Arguments:

tag()関数は簡略化しているが、おおよそのイメージは掴めるかと思う。実際にはこの中で、引数に応じて関数名に対応するDOMオブジェクトを作って返す。

Proxyを入れ子にすることで、名前空間とタグ名を効率的にまとめていることがわかる。かっちょいい。

余談だがVanJSは140行でUIフレームワークの基本的な機能を実現している。ソースはこちら。超かっちょいい。

参考資料