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

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

# Proxyオブジェクトって何？

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

こう言った拡張するパターンは[関数フッキング](https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%83%E3%82%AF_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0))、またはAOPなどと呼ばれたりします。
プロキシには**代理**という意味がありますね。

## 構文

```javascript:Proxy構文
new Proxy(target,handler)
```
`traget`・・・操作を差し込む対象のオブジェクト
`handler`・・・ターゲットに対する操作を表すオブジェクト

## 例題

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

```javascript: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: "わん"} 
```

が返ってきます。ちなみにこれは

```javascript
console.log(myProxy);
```

の時と同じものが返ってきます。プロキシ自体ということですね。

##Proxyの実践
getトラップだけでいうと、オブジェクトの全てのプロパティに一律に処理することができますね。
上の例では、**プロパティにデフォルト値を与える**という機能を果たしています。

実践的な例では、
- データが妥当かどうかバリデーションをかける
- 権限の確認をする
- キャッシュを保存する
- ロギング
- 遅延処理

などに役立てることができます。

実際の使用例では、
[immer.js](https://github.com/immerjs/immer)の実装部分で確認できました。
JSでimmutableを実現するのはコードが複雑になりがちですが、Setにimmutableの処理を挟み込んで、あたかも代入しているだけのように記述できるという面白い使い方をされています。

他のトラップの実装も見てみましょう。

###setトラップ（設定するプロパティを操作）
いくつかのトラップには実装に関して条件があり、[[Set]]は書き込みが正常な場合は`true`、正常でない場合は`false`のbooleanを返却する必要があります。

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

```javascript:Reflectnの例
const myObj = {};
myObj.test = 3;

console.log(Reflect.has(myObj, "test")); // true
```
上記では`in演算子`と同じ動きを行なっています。
`Reflect`はエラーを発生させないという特徴があります。

```javascript:普通のエラー挙動
const myObj = {};
Object.defineProperty(myObj,'foo',{
    value: 'neko',
    writable: false
});
Object.defineProperty(myObj,'foo',{
    value: 'neko',
    writable: true
});
```

上記では再定義で`writable:true`にしているのでエラーになります。
しかし、`Reflect`だと、

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

```javascript: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);  //そんなのいないよ
```

そして、もう１つの`Reflect`の存在価値ですが、第３引数の`receiver`が使えることです。

#Reflectのreceiverを使いこなす
```javascript: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`で呼び出しているから`this`は`myProxy`をさして欲しい（ここでの戻り値が`そんなのはいない。`にしたい）ような時は困ります。
そこで`Reflect`の出番です。

```javascript: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