JavaScript(ES2015〜)のProxyで、プロパティにフックする正しいやり方


この記事について

ES2015(ES6)で追加されたProxyオブジェクトを使用すると、プロパティへのアクセスに割り込んで、好きな処理を行うことができます。

ただし、使い方に少しコツがあります。この記事では、ネット上で散見される「良くない例」について、なにがどう良くないのかを説明して、気をつけるべきポイントを紹介します。

良くない例

こういう感じのコードが紹介されている場合がありますが、あまり良くありません。

/*
常に値を2倍にするProxy
*/
function doubleProxy_Bad(target) {
    return new Proxy(target, {
        get(target, name) {
            //もとのプロパティの値を取得
            const orig = target[name];
            //2倍にして返す
            return orig * 2;
        },  
        set(target, name, value) {
            //値を2倍にする
            const modified = value * 2;
            //もとのプロパティに設定
            target[name] = modified;
        }   
    }); 
}

正しい例

より正しくは、こうです。

function doubleProxy_Good(target) {
    return new Proxy(target, {
        get(target, name, receiver) {
            //もとのプロパティの値を取得
            const orig = Reflect.get(target, name, receiver);
            //2倍にして返す
            return orig * 2;
        },  
        set(target, name, value, receiver) {
            //値を2倍にする
            const modified = value * 2;
            //もとのプロパティに設定
            Reflect.set(target, name, modified, receiver);
        }   
    }); 
}

違いは次の2点です。

  • receiver引数を正しく処理している(get/setの最後の引数)
  • Reflectオブジェクトを使っている

get/set には receiver という引数がある

receiverの正体は、Proxyオブジェクトそのものです。次のコードで確認できます。

const o = { a: 10 };

//どんなプロパティにアクセスしてもreceiverを返す
const p = new Proxy(o, {
    get(target, name, receiver) {
        return receiver;
    }
});

console.log(p.a === p);

一見すると、この引数が何のために存在するのか、わからないかもしれません。しかしgetter/setterが定義されている場合には、大きな違いが出ます。

次のような例を考えましょう。

const obj = {
    a: 1,
    b: 5,
    get c() {
        return this.a + this.b;
    }
};

const p = doubleProxy(obj);

console.log(p.c); //=> ???

p.cの結果は、12でしょうか?24でしょうか?

言いかえると、get cの中のthisは、もとのオブジェクトでしょうか?それともProxyでしょうか?

最初に紹介した「良くない例」だと、thisは必ずもとのオブジェクトになり、結果は12になります。「正しい例」では、thisはProxyになり、24になります。

console.log(doubleProxy_Bad(obj).c);  //=> 12
console.log(doubleProxy_Good(obj).c); //=> 24

どちらの動作が望ましいかは、ケースバイケースです。しかしProxyでないと困る、というケースはあるでしょう。

たとえばgetter/setterの中でメソッドを呼び出す場合には、thisの値によって、そのメソッド呼び出しにもProxyの効果が及ぶかどうかが決まります。

Reflectオブジェクトを使おう

receiver引数とReflectオブジェクトは、そのためにあります。

Reflect.get(target, name[, receiver]);
Reflect.set(target, name, value[, receiver]);

Reflect.getReflect.setの引数にreceiverを与えると、その値がgetter/setter呼び出しのthisになります。

省略すると、targetがthisになります。つまりもとのオブジェクトを直接参照するのと同じです。

どちらが自分の望む動作か、よく考えて使いわけましょう。

まとめ

ProxyとReflectは、セットで覚えておきましょう。get/setに限らず、両者のAPIは統一されており、組み合わせて使うよう設計されています。

thisが何を指しているか」は、JavaScriptを書く上で、常に意識すべきポイントです。getter/setterという、比較的新しい要素についても、忘れないようにしましょう。

参考URL

ES2015(ES6)

ES2016(ES7)

ES Wiki