What is Proxy

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

オブジェクトをラップし、オブジェクトが持つ基本的な機能(obj.keyと書いてvalueを取得する機能など)を上書くことができるのがProxy Objectです。
(高機能なオブジェクトを生成することができる、けっこうイカした仕様だと思います😋)

つかいかた

詳細な用法やパラメータの説明は MDNに任せるとして、基本的なSyntaxだけ記します。

let proxy = new Proxy(target, handler);
  • target …Proxyでラップする対象となるObject
  • handler…上書く関数をプロパティに持つObject

用例

実際のソースをみたほうがピンとくるかもしれません💡

let obj = {
  name: 'Aloerina',
  age: 17
};

const handler = {
  set: function(target, prop, value) {
    console.error(`${prop}は上書き禁止です`);
  }
};

let proxy = new Proxy(obj, handler);  // objをラップしたproxyを生成する

proxy.name = 'Margherita';  // nameは上書き禁止です
concole.log(proxy.name);    // Aloerina

Handlerの種類

オブジェクトが持つ基本的な機能、つまり「オブジェクトのプロパティへのアクセスを提供するメソッド」のことをトラップといいます。
handlerとは、トラップの処理を上書きするものなので、各トラップに対して対応するhandlerが用意されています。「Proxyで何ができるか」を知りたいときはこれを見るのが分かりやすそうです。

handler trap
handler.getPrototypeOf(target) Object.getPrototypeOf
handler.setPrototypeOf(target, prototype) Object.setPrototypeOf
handler.isExtensible(target) Object.isExtensible
handler.preventExtensions(target) Object.preventExtensions
handler.getOwnPropertyDescriptor(target, prop) Object.getOwnPropertyDescriptor
handler.defineProperty(target, prop, descriptor) Object.defineProperty
handler.has(target, prop) in operator
handler.get(target, prop, receiver) getting property values
handler.set(target, prop, value, receiver) setting property values
handler.deleteProperty(target, prop) delete operator
handler.ownKeys(target) Object.getOwnPropertyNames
Object.getOwnPropertySymbols
handler.apply(target, thisArg, argumentsList) function call
handler.construct(target, argumentsList, newTarget) new operator

Reflect

handlerの中で「本来の処理を呼び出したい」場面があると思います。この場合に使えるのがReflectです。handlerと同じメソッド群を持ちます。

const handler = {
  set: function(target, prop, value) {
    console.log('Before set');
    Reflect.set(target, prop, value);  // targetオブジェクトのsetterを呼び出す
  }
};

let proxy = new Proxy({}, handler);

proxy.name = 'Margherita';  // Before set
concole.log(proxy.name);    // Margherita

Can I Use

執筆時点(2017/03/14)でのものです。

caniuse

Polyfill

GoogleChrome/proxy-polyfill

このPolyfillでは

  • get
  • set
  • apply
  • construct

のトラップにしか対応していないので、その点ご注意を…。

以上、ざっとProxyの使い方でした。ここからはProxyの実用例の話です。

[Tips] EnumObjectをつくる! EnumBuilder

Enumの定義にオブジェクトを使うことケース。よくあります。
あるオブジェクトがEnumである以上、値が書き換えられないことが厳密に管理されていると良いです。ということで、オブジェクトがEnumとして生まれ変わるEnumBuilderをつくってみました。

  • set / deleteができないオブジェクトにする
  • importしてType.Inportのようにスタティックアクセス風に使える
  • 既に作られているEnum用オブジェクトにも簡単に適用できる
  • オブジェクトの中にオブジェクトが入れ子になるケースは想定してない

といったことを満たす仕様にしたくてこのような形にしました。ただ、渡すオプションがこれ以上増えなさそうなのでBuilderパターンにするのは過剰な実装だったかもしれません。すみません好みです🙇

[Tips] 用途別に制約付きModuleを定義してクラスベースな実装をする!

class構文で用意したModuleをimportしてnewして使うと本当にクラスをインスタンス化してるように見えて、読みやすいクラスベース(風)の実装となりますよね。
しかし実際にはインスタンス化されたソレはただのオブジェクトです。やろうと思えばfunctionを外から追加することも、privateな用途で用意した変数を強引に書き換えることもできてしまいます。

import User from './User';

let user01 = new User();
user01._name = 'Aloerina';        // やめてー! private用の変数を書き換えないでー!

こうなってしまうと悲惨なので😭、Module毎の性質に応じて機能を制約し、不本意な使われ方を防ぐため、

  1. Private変数を持つModule
  2. Immutableな値を定義するModule
  3. SingletonなModule

の3パターンのModuleを定義するためのModuleBuilderを作ってみました。

  • 既に作られているModuleにも適用できる
  • Moduleのソース内に、どの性質を持っているのかが明記されている
  • 今まで通りModuleをimportしてnewして使える。呼び出し側で修正が不要(singletonは除く)
  • 上記3つ以外にもパターンを拡張できる

といったことを意識してつくってみました。Builderパターンなのは言わずもがな。
任意のクラスと用意されたhandlerを選択してBuilderに渡し、出来上がったProxyをModuleとしてExportさせて使います。以下に実用例を2つ挙げます。

Immutableなオブジェクト

Singletonなオブジェクト

ちなみに、設計当初はHandlersを組み合わせていろんな性質を持つModuleをつくれるようにしたい(Javaのlombokアノテーションみたいな感じで簡単に付与できるようにしたい)と思っていたのですが、handlerの競合が起きるのでやめました…。

おわりに

オブジェクトが持つ基本機能を上書くことでJavaScript特有の自由さに制限を設けて堅牢な実装をしよう! というお話でした。

とは言え、やや強引な実装なので粗があることも事実です(上記の例ではSingletonの実装は微妙。そもそもJSでSingletonなんて使わなくてもいいかもなと思ったり…)。
クラスベースな書き方をゴリ押しで実現しているとも言えるので、そもそもJavaScriptの実装としてふさわしいかという議論の余地はありそうです。

ただ、そういった点も踏まえた上で使うのであれば強力な効果を発揮してくれるのがProxyだと思います。今回は自分で試用してみたものだけをまとめましたが、他にも有用な使い方はあるようです。下記のサイトが参考になりました。

ES6 Features - 10 Use Cases for Proxy

もっと良い実装がある、間違っている部分がある、という場合はぜひ@aloerina_ までご一報いただけると嬉しいです。

現場からは以上です。