53
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-12

*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

53
37
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?