*2021/9/20に大幅にリライトしました。
Proxyオブジェクトって何?
オブジェクトの基本操作(プロパティの設定や、値の列挙)を、
拡張してくれるオブジェクトです。
ES2015から追加されました。
こう言った拡張するパターンは関数フッキング、またはAOPなどと呼ばれたりします。
プロキシには代理という意味がありますね。
構文
new Proxy(target,handler)
traget
・・・操作を差し込む対象のオブジェクト
handler
・・・ターゲットに対する操作を表すオブジェクト
例題
const animal = { neko:'にゃん', inu:'わん' };
var proxy = new Proxy(animal , {
get(target, prop) {
return prop in target ? target[prop] : 'そんなのいないよ';
}
});
console.log(proxy.inu); //わん
console.log(proxy.helicopter); //そんなのいないよ
三行目のget()について
proxyのハンドラーで定義できるメソッドはあらかじめ決められていて
トラップ
と呼ばれています。
メソッド(トラップ) | 内部メソッド | 操作内容 |
---|---|---|
get(target,prop,receiver) | [[Get]] | プロパティの取得(receiverはプロキシ) |
set(target,prop,val,receiver) | [[Set]] | プロパティの設定 |
has(target,prop) | [[HasProperty]] | in演算子によるメンバー存在確認 |
deleteProperty(target,prop) | [[Delete]] | delete命令によるプロパティの削除 |
getPrototypeOf(target) | [[GetPrototypeOf]] | プロトタイプの取得 |
setPrototypeOf(target,prototyoe) | [[SetPrototypeOf]] | プロトタイプの設定 |
construct(target,args) | [[Construct]] | new演算子によるインスタンス化 |
apply(target,thisArg,args) | [[Call]] | applyメソッドによる関数呼び出し |
つまり例文で行われている内容は、ターゲットの中にプロパティがあるかをチェックして、
存在していればtarget[prop]
つまりその値を返却、存在していなければそんなのいないよ
を返却しています。
こういった形で既存のオブジェクトのメソッドの機能を拡張することが可能です。
##トラップについてもっと知りたい
get(target,prop,receive)
のreceiveってなんなんでしょう。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
こちらを見ると、proxy、または proxy から継承するオブジェクトのどちらかということです。
*handlerの設定がない場合、proxyはオブジェクトを単にラップして返却します。
const animal = { neko:'にゃん', inu:'わん' };
const myProxy = new Proxy(animal , {
get(target, prop, receive){
console.log(receive);
return prop in target ? target[prop] : 'そんなのいないよ';
}
});
console.log(myProxy.neko);
上のコードをちょっといじって、receiveを表示させるようにしました。
Proxy{neko: "にゃん", inu: "わん"}
が返ってきます。ちなみにこれは
console.log(myProxy);
の時と同じものが返ってきます。プロキシ自体ということですね。
##Proxyの実践
getトラップだけでいうと、オブジェクトの全てのプロパティに一律に処理することができますね。
上の例では、プロパティにデフォルト値を与えるという機能を果たしています。
実践的な例では、
- データが妥当かどうかバリデーションをかける
- 権限の確認をする
- キャッシュを保存する
- ロギング
- 遅延処理
などに役立てることができます。
実際の使用例では、
immer.jsの実装部分で確認できました。
JSでimmutableを実現するのはコードが複雑になりがちですが、Setにimmutableの処理を挟み込んで、あたかも代入しているだけのように記述できるという面白い使い方をされています。
他のトラップの実装も見てみましょう。
###setトラップ(設定するプロパティを操作)
いくつかのトラップには実装に関して条件があり、[[Set]]は書き込みが正常な場合はtrue
、正常でない場合はfalse
のbooleanを返却する必要があります。
const myObj = new Proxy({},{
set(target, name, value, receiver){
if(isNaN(value)) {
return false;
} else {
target[name] = value;
return true;
}
}
});
myObj.foo = 3;
myObj.bar = 'だめ';
console.log(myObj.foo) // 3
console.log(myObj.bar) // undefined
setトラップの引数についてですが、
target
・・・カスタマイズする引数
name
・・・プロパティ名
value
・・・代入される値
receiver
・・・上で説明したのと同じ(プロキシ)
です。
このコードでは、数値以外の値が代入されるとfalseが帰り、エラーになるというものです。
##正しいproxyって何だろう。Reflectって?
https://qiita.com/tkykmw/items/6981edef82fed25d370a
こんな記事を見つけてしまいました。曰く、
receiver引数を正しく処理している(get/setの最後の引数)
Reflectオブジェクトを使っている
というものが正しいproxyらしいです。むむむ?
ということでReflect
についても覚えます。
Reflect
・・・組み込みオブジェクト。proxyで使われるトラップをメソッドとして使える。
const myObj = {};
myObj.test = 3;
console.log(Reflect.has(myObj, "test")); // true
上記ではin演算子
と同じ動きを行なっています。
Reflect
はエラーを発生させないという特徴があります。
const myObj = {};
Object.defineProperty(myObj,'foo',{
value: 'neko',
writable: false
});
Object.defineProperty(myObj,'foo',{
value: 'neko',
writable: true
});
上記では再定義でwritable:true
にしているのでエラーになります。
しかし、Reflect
だと、
var myObj = {};
Object.defineProperty(myObj,'foo',{
value:'neko',
writable: false
});
console.log(Reflect.defineProperty(myObj,'foo',{
writable: true, //戻り値:false エラーにならない
}));
console.log(Object.getOwnPropertyDescriptor(myObj,'foo'));
//{value: "neko", writable: false, enumerable: false, configurable: false}
//writableは変更はされていない
上記のように、 エラーにはなりません。
##ReflectとProxy
Proxy使用例で紹介したコードでは、プロパティがあるものを参照されたら、それを返し、そうでないものは独自のエラーメッセージを出すというものでした。
このように、オブジェクト本来の動きをプロキシに組み込みたい時にReflect
は便利です。
let animal = {neko:'にゃん', inu:'わん'};
var proxy = new Proxy(animal , {
get(target,prop){
return prop in target ? Reflect.get(target,prop) : 'そんなのいないよ';
}
});
console.log(proxy.inu); //わん
console.log(proxy.helicopter); //そんなのいないよ
そして、もう1つのReflect
の存在価値ですが、第3引数のreceiver
が使えることです。
#Reflectのreceiverを使いこなす
const animals = {
neko:'にゃー',
inu:'わん',
get listen(){ return this.kirin; }
};
const myProxy = new Proxy(animals, {
get(target, name) {
if(name in target) {
console.log(name)
console.log(target)
console.log(name in target)
return target[name] + 'と鳴いた';
} else {
return 'そんなのはいない。';
}
},
})
console.log(myProxy.listen); //undefineと鳴いた。
ゲッターの中の**this
**に注目です。
myProxy.listen
が実行されると、myProxyのgetトラップ
が実行され、
target[name] + 'と鳴いた'
が返されます。
なのでlistenで使われているthis
はanimalsのthis、つまりwindow
をさしています。
これは直感的ではなく、myProxy
で呼び出しているからthis
はmyProxy
をさして欲しい(ここでの戻り値がそんなのはいない。
にしたい)ような時は困ります。
そこでReflect
の出番です。
const animals = {
neko:'にゃー',
inu:'わん',
get listen(){ return this.kirin;}
};
const myProxy = new Proxy(animals,{
get(target,name,receiver){
if(name in target){
return Reflect.get(target,name,receiver);
}else{
return 'そんなのはいない。';
}
},
})
console.log(myProxy.listen); //そんなのはいない。
receiver
を使うことで、getter/setter呼び出し時のthis
が
myProxy
を差すようにになってくれました。
ケースによってreceiverを使うかどうかを考えた方が良さそうです。
参考文献
https://uhyohyo.net/javascript/16_14.html
http://dealwithjs.io/es6-features-10-use-cases-for-proxy/
https://aloerina01.github.io/blog/2017-03-14-1
https://ja.javascript.info/proxy