🐬

【後半】ProxyオブジェクトとReflectオブジェクト

2022/03/14に公開

どうもフロントエンドエンジニアのoreoです。

前回の記事(👇)では、Proxyオブジェクトに関して整理を行いました。今回の記事では、Reflectオブジェクトに関して、Proxyオブジェクトとの併用方法なども含めて整理したいと思います!
https://zenn.dev/oreo2990/articles/6e4dc6c1eb48c3

1 Reflectオブジェクトとは

Reflectオブジェクトは、JavaScriptエンジン内部メソッドを呼び出すメソッドが格納されているオブジェクトです。

JavaScriptエンジンでは、その内部でのみ使用する内部メソッドを保持しており、Reflectオブジェクトを使うことで、それらの内部メソッドに関数表記でアクセスすることが可能となります。

【内部メソッド、Reflect、オブジェクト操作の対応表】

内部メソッド Reflect呼び出し オブジェクトの操作
[[Get]] Reflect.get(obj, prop) obj[prop]
[[Set]] Reflect.set(obj, prop, value) obj[prop] = value
[[Delete]] Reflect.deleteProperty(obj, prop) delete obj[prop]
[[Construct]] Reflect.construct(F, value) new F(value)
[[HasProperty]] Reflect.hsa(obj, value) prop in obj

参考 https://ja.javascript.info/proxy#ref-7

2 Reflectオブジェクトのメソッド

2-1 getメソッド

get メソッドを使用することで、オブジェクトのゲッターへのアクセスを関数形式で行うことができます。

/**
 * @param target         対象オブジェクト
 * @param propertyKey    アクセスするtargetのプロパティのキー
 * @param receiver       target[propertyKey]がゲッターの場合、ゲッターのthisが、receiverとして渡したオブジェクトに束縛される
 */
Reflect.get(target,propertyKey,receiver)

以下のような場合、dog.bowWow;で、ゲッターにアクセスすることができます。

この処理は、JavaScriptエンジン内部では[[Get]]を実行しています。[[Get]]Reflectオブジェクトのget メソッドで呼び出すことが可能なので、Reflect.get(dog,"bowWow") で、dog.bowWow同様の処理を実行可能です。

const dog = {
  name: "犬",
  get bowWow() {
    console.log(`${this.name}が吠えた`);
		return this.name;
  },
};
dog.bowWow;   //「犬が吠えた」が出力

Reflect.get(dog,"bowWow")   //「犬が吠えた」が出力

また、get メソッドの第三引数にオブジェクトを渡すと、ゲッターのthisを渡したオブジェクトに束縛することが可能です。

const dog = {
  name: "犬",
  get bowWow() {
    console.log(`${this.name}が吠えた`);
		return this.name;
  },
};
const cat = {
  name: "猫",
};
Reflect.get(dog,"bowWow")        //「犬が吠えた」が出力。
Reflect.get(dog,"bowWow",cat)    //「猫が吠えた」が出力。thisがcatオブジェクトを参照。

2-2 setメソッド

set メソッドを使用することで、オブジェクトのセッターを用いた値の設定を関数形式で行うことができます。

/**
 * @param target         対象オブジェクト
 * @param propertyKey    設定したいtargetのプロパティのキー
 * @param value          設定するtargetのプロパティの値
 * @param receiver       target[propertyKey]がセッターの場合、セッターのthisが、receiverとして渡したオブジェクトに束縛される
 */
Reflect.set(target, propertyKey, value, receiver)

以下のような場合、dog.named = "イギー";で、セッターを用いて値の設定を行うことができます。

この処理は、JavaScriptエンジン内部では[[Set]]を実行しています。[[Set]]Reflectオブジェクトのset メソッドで呼び出すことが可能なので、Reflect.set(dog, "name", '承太郎');で、値の設定が可能です。

const dog = {
  name: "犬",
  set named(val) {
    this.name = val;
  },
};
console.log(dog.name);  //「犬」が出力

dog.named = "イギー";
console.log(dog.name);  //「イギー」が出力

Reflect.set(dog, "name", '承太郎');
console.log(dog.name);  //「承太郎」が出力

また、set メソッドの第四引数にオブジェクトを渡すと、セッターのthisを渡したオブジェクトに束縛することが可能です。

const dog = {
  name: "犬",
  set named(val) {
    this.name = val;
  },
};
const cat = {
  name: "猫",
};

console.log(dog.name);  //「犬」が出力。
console.log(cat.name);  //「猫」が出力。

Reflect.set(dog, "name", "イギー", cat);

console.log(dog.name);  //「犬」が出力。
console.log(cat.name);  //「イギー」が出力。thisがcatオブジェクトを参照。

2-3 construct メソッド

construct メソッドを使用することで、new演算子と同様の処理を行うことが可能です。

/**
 * @param target         コンストラクター関数
 * @param argumentsList  target(コンストラクター関数)に渡す引数の配列
 */
Reflect.construct(target,argumentsList)

例えば、下記のようにAnimalsクラスをnew演算子でインスタンス化する際、JavaScriptエンジンでは[[Construct]]を実行しています。

[[Construct]]は、Reflectオブジェクトのconstruct メソッドで呼び出すことが可能なので、Reflect.construct(Animals, ["dog", "cat"])で、new演算子と同様にインスタンス化することができます。

class Animals {
  constructor(animal_1, animal_2) {
    this.animal_1 = animal_1;
    this.animal_2 = animal_2;
  }
}
const animals_1 = new Animals("dog", "cat");
console.log("animals_1は", animals_1);

const animals_2 = Reflect.construct(Animals, ["dog", "cat"]);
console.log("animals_2は", animals_2);

コンソールで確認してみると、animals_1animals_2には、Animalsクラスのインスタンスが格納されます。

2-4 has メソッド

has メソッドを使用することで、in演算子と同様の判定を行うことが可能です。

/**
 * @param target         対象オブジェクト
 * @param propertyKey    targetにあるかどうか判定したいプロパティーのキー
 */
Reflect.has(target,propertyKey)

in演算子では、"dog" in animalsのような形で、animalsオブジェクトに"dog"プロパティーが存在するかをチェックすることが可能でした。これは、JavaScriptエンジン内部では[[HasProperty]]を実行しおり、[[HasProperty]]Reflectオブジェクトのhas メソッドで呼び出すことが可能なので、Reflect.has(animals,"dog")で、in演算子と同様の判定ができます。

const animals ={
  dog:"犬",
  cat:"猫"
}
console.log("dog" in animals)   //true
console.log("bird" in animals)  //false

console.log(Reflect.has(animals,"dog"))   //true
console.log(Reflect.has(animals,"bird"))  //false

2 ProxyオブジェクトとReflectオブジェクト

内部メソッド、ProxyReflectは全て対になっています。ProxyReflectと合わせて使うことで、オブジェクトの拡張がより柔軟に記載できます。

【内部メソッド、ReflectProxyトラップの対応表】

内部メソッド Reflect呼び出し Proxyトラップ
[[Get]] Reflect.get(obj, prop) get
[[Set]] Reflect.set(obj, prop, value) set
[[Delete]] Reflect.deleteProperty(obj, prop) deleteProperty
[[Construct]] Reflect.construct(F, value) construct
[[HasProperty]] Reflect.hsa(obj, value) has

ここから前回の記事の2-3で記載した、getトラップを使ってデフォルト値を返す場合において、Reflectを用いた記載に書き換えてみます。

まず、Proxyのみの記載です。

/**
 * new Proxyに渡すhandlerに、getトラップを定義。
 * アクセスしようとするプロパティーが存在する場合は、その値を返す。
 * アクセスしようとするプロパティーが存在しない場合は、"hoge"を返す。
 */
const targetObj = { a: 0 };

const handler = {
  get: function (target, prop, receiver) {
    if (prop in target) {  
      return target[prop];
    } else {
      return "hoge";        
    }
  },
};

const proxy = new Proxy(targetObj, handler);
console.log(proxy.a);       //「0」が出力
console.log(proxy.b);       //「hoge」が出力。

target[prop]は、Reflect.get(target,prop)に置き換えることが可能です。

const targetObj = { a: 0 };

const handler = {
  get: function (target, prop, receiver) {
    if (prop in target) {  
      return Reflect.get(target,prop);   //Reflectオブジェクトを使用する形に変更
    } else {
      return "hoge";        
    }
  },
};

const proxy = new Proxy(targetObj, handler);
console.log(proxy.a);       //「0」が出力
console.log(proxy.b);       //「hoge」が出力。

では、targetObjにゲッターを追加し、ゲッター経由で値を取得する場合を考えます。この場合は問題ありません。

const targetObj = {
  a: 0,
  get getVal() {
    return this.a;   //ゲッターを追加
  },
};

const handler = {
  get: function (target, prop, receiver) {
    if (prop in target) {
      return Reflect.get(target, prop);
    } else {
      return "hoge";
    }
  },
};

const proxy = new Proxy(targetObj, handler);
console.log(proxy.getVal);   //「0」が出力。ゲッター経由で値を取得。
console.log(proxy.b);      //「hoge」が出力。

しかし、ゲッターの返す値をthis.b変更するとconsole.log(proxy.getVal)の出力値が、hogeではなく、undefinedとなります。これは、ゲッターのthisは、proxyオブジェクトでなくtargetObjそのものを参照するためです。

const targetObj = {
  a: 0,
  get getVal() {
    return this.b;    //this.bに変更
  },
};

const handler = {
  get: function (target, prop, receiver) {
    if (prop in target) {
      return Reflect.get(target, prop);
    } else {
      return "hoge";
    }
  },
};

const proxy = new Proxy(targetObj, handler);
console.log(proxy.getVal);   //「undefined」が出力。
console.log(proxy.b);        //「hoge」が出力。

存在しないプロパティーに対してhogeを返すには、Reflect.get()に、getトラップ内でのreceiver(proxyオブジェクトそのもの)を渡し、ゲッターのthisを、proxyオブジェクトに束縛することで実現できます。

const targetObj = {
  a: 0,
  get getVal() {
    return this.b;
  },
};

const handler = {
  get: function (target, prop, receiver) {
    if (prop in target) {
      return Reflect.get(target, prop,receiver);  //receiverを渡す
    } else {
      return "hoge";
    }
  },
};

const proxy = new Proxy(targetObj, handler);
console.log(proxy.getVal);    //「hoge」が出力。
console.log(proxy.b);         //「hoge」が出力。

このように、ProxyオブジェクトとReflectオブジェクト合わせて使うことで、より高度なオブジェクト操作ができます。

最後に

2回にわけて、ProxyオブジェクトとReflectオブジェクトに関して整理しました。両方を併用することで、柔軟なオブジェクトの拡張ができそうですね。

あまり使ったことがないので、日々の実装で意識し、定着させていきたいです!

参考

Proxy と Reflect

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

Proxy - JavaScript | MDN

Reflect - JavaScript | MDN

Discussion