ES6 Proxy をつかって堅牢なオブジェクトをつくるTips
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でラップする対象となるObjecthandler
…上書く関数をプロパティに持つ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は上書き禁止です
console.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
console.log(proxy.name); // Margherita
Can I Use
執筆時点(2017/03/14)でのものです。
Polyfill
このPolyfillでは
- get
- set
- apply
- construct
のトラップにしか対応していないので、その点ご注意を…。
以上、ざっとProxyの使い方でした。ここからはProxyの実用例の話です。
[Tips] EnumObjectをつくる! EnumBuilder
Enumの定義にオブジェクトを使うケース、よくありますよね。
あるオブジェクトがEnumである以上、値が書き換えられないことが厳密に管理されていると良いです。ということで、オブジェクトがEnumとして生まれ変わるEnumBuilder
をつくってみました。
set / delete
ができないオブジェクトにするimport
してType.INSERT
のようにスタティックアクセス風に使える- 既に作られているEnum用オブジェクトにも簡単に適用できる
- オブジェクトの中にオブジェクトが入れ子になるケースは想定してない
といったことを満たす仕様にしたくてこのような形にしました。
[Tips] 用途別に制約付きModuleを定義してクラスベースな実装をする!
class構文で用意したModuleをimport
してnew
して使うと本当にクラスをインスタンス化してるように見えて、読みやすいクラスベース(風)の実装となりますよね。
しかし実際にはインスタンス化されたソレはただのオブジェクトです。やろうと思えばfunctionを外から追加することも、privateな用途で用意した変数を強引に書き換えることもできてしまいます。
import User from './User';
let user01 = new User();
user01._name = 'Aloerina'; // やめてー! private用の変数を書き換えないでー!
こうなってしまうと悲惨なので、Module毎の性質に応じて機能を制約し、不本意な使われ方を防ぐため、
- Private変数を持つModule
- Immutableな値を定義するModule
- 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_ までご一報いただけると嬉しいです。
現場からは以上です。