28
Help us understand the problem. What are the problem?

posted at

updated at

Organization

【ES6】Proxyオブジェクトについて

*2021/9/20に大幅にリライトしました。

Proxyオブジェクトって何?

オブジェクトの基本操作(プロパティの設定や、値の列挙)を、
拡張してくれるオブジェクトです。
ES2015から追加されました。

こう言った拡張するパターンは関数フッキング、またはAOPなどと呼ばれたりします。
プロキシには代理という意味がありますね。

構文

Proxy構文
new Proxy(target,handler)

traget・・・操作を差し込む対象のオブジェクト
handler・・・ターゲットに対する操作を表すオブジェクト

例題

Proxy使用例
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はオブジェクトを単にラップして返却します。

getのreceive検証
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を返却する必要があります。

setトラップ例
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で使われるトラップをメソッドとして使える。

Reflectnの例
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だと、

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は便利です。

Prozy使用例(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を使いこなす

getter/setterの時のthis
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で呼び出しているからthismyProxyをさして欲しい(ここでの戻り値がそんなのはいない。にしたい)ような時は困ります。
そこでReflectの出番です。

Reflectのreceiver
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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
28
Help us understand the problem. What are the problem?