【JavaScript】Reflect オブジェクトを理解する

reflect JavaScript

Reflect オブジェクトとは

Reflect オブジェクトは JavaScript エンジンが内部で使用している汎用的な関数(内部メソッド)を格納しているオブジェクトであり、ES6 から導入された。

ES5 までは内部メソッドをコードから明示的に呼び出すことはできなかったが、ES6 からは Reflect オブジェクトを通して呼び出すことができるようになった。

内部メソッドと Reflect オブジェクト、およびオブジェクト操作の対応関係は次のようになっている。(参考:ECMAScriptMDN

内部メソッドReflect からの
明示的な呼び出し
対応する
オブジェクトの操作
[[Call]]Reflect.apply(func, thisArg, args)関数呼び出し
[[Construct]]Reflect.construct(F, args)new F(...args)
[[DefineOwnProperty]]Reflect.defineProperty(
obj, prop, descriptor)
Object.defineProperty(
obj, prop, descriptor)
[[Delete]]Reflect.deleteProperty(obj, prop)delete obj[prop]
[[Get]]Reflect.get(obj, prop)obj[prop]
[[GetOwnProperty]]Reflect.getOwnPropertyDescriptor(
obj, prop)
Object.getOwnPropertyDescriptor(
obj, prop)
[[GetPrototypeOf]]Reflect.getPrototypeOf(obj)Object.getPrototypeOf(obj)
[[HasProperty]]Reflect.has(obj, prop)prop in obj
[[IsExtensible]]Reflect.isExtensibleObject.isExtensible(obj)
[[OwnPropertyKeys]]Reflect.ownKeys(obj)Object.getOwnPropertyNames(obj)
.concat(Object.getOwnPropertySymbols(obj))
[[PreventExtensions]]Reflect.preventExtensions(obj)Object.preventExtensions(obj)
[[Set]]Reflect.set(obj, prop, value)obj[prop] = value
[[SetPrototypeOf]]Reflect.setPrototypeOf(obj, prototype)Object.setPrototypeOf(obj, prototype)

Reflect のメソッド

Reflect オブジェクトが持つメソッドのいくつかをサンプルを用いて説明する。

Reflect.construct() メソッド

Reflect.construct() メソッドを使用すると、new 演算子によるインスタンス生成と同等のことを、関数の記述形式で行うことができる。

class MyClass {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  profile() {
    console.log(`name: ${this.name}, age: ${this.age}`);
  }
}

const obj1 = new MyClass('taro', 32);
console.log(obj1);
// MyClass {name: "taro", age: 32}

const obj2 = Reflect.construct(MyClass, ['taro', 32]);
console.log(obj2);
// MyClass {name: "taro", age: 32}

Reflect.has() メソッド

Reflect.has() メソッドを使用すると、in 演算子によるプロパティの判定と同等のことを、関数の記述形式で行うことができる。

class MyClass {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  profile() {
    console.log(`name: ${this.name}, age: ${this.age}`);
  }
}

const obj = new MyClass('taro', 32);
console.log(obj);
// MyClass {name: "taro", age: 32}

console.log('name' in obj);     // true
console.log('age' in obj);      // true
console.log('profile' in obj);  // true
console.log('country' in obj);  // false

console.log(Reflect.has(obj, 'name'));    // true
console.log(Reflect.has(obj, 'age'));     // true
console.log(Reflect.has(obj, 'profile')); // true
console.log(Reflect.has(obj, 'country')); // false

Reflect.get() メソッド

Reflect.get() メソッドの基本

Reflect.get() メソッドを使用すると、オブジェクトのプロパティの読み出しアクセス obj.prop を関数形式で行うことができる。

const tom = {
  _name: 'Tom',
  introduce: function() {
    console.log('this:', this);
    console.log(`My name is ${this._name}`);
  },
  get name() {
    console.log('this:', this);
    return this._name;
  }
}

console.log(tom._name);
// Tom
console.log(Reflect.get(tom, '_name'));
// Tom

tom.introduce();
// this: {_name: "Tom", introduce: ƒ}
// My name is Tom
Reflect.get(tom, 'introduce')();
// this: Window {window: Window, self: Window, document: document, name: "", location: Location, …}
// My name is undefined
Reflect.get(tom, 'introduce').call(tom);
// this: {_name: "Tom", introduce: ƒ}
// My name is Tom

console.log(tom.name);
// this: {_name: "Tom", introduce: ƒ}
// Tom
console.log(Reflect.get(tom, 'name'));
// this: {_name: "Tom", introduce: ƒ}
// Tom

ただし、このサンプルから分かるように、参照したプロパティが関数(メソッド)の場合、Reflect.get() メソッドで取得される関数は、実行するときに call メソッドなどを使用して this を明示的に指定する必要があるので注意が必要だ。

Reflect.get() メソッドの第3引数 receiver

Reflect.get() メソッドは第3引数 receiver を指定することもできる。第1引数 targetObj と第2引数 prop で指定したオブジェクトのプロパティの値 targetObj[prop] がゲッターのとき、そのゲッターの処理内の this を第3引数で指定することができる。第3引数を指定しなかった場合は、ゲッターの this は第1引数が使用される。

const tom = {
  _name: 'Tom',
  introduce: function() {
    console.log('this:', this);
    console.log(`My name is ${this._name}`);
  },
  get name() {
    console.log('this:', this);
    return this._name;
  }
}

console.log(tom.name);
// this: {_name: "Tom", introduce: ƒ}
// Tom
console.log(Reflect.get(tom, 'name'));
// this: {_name: "Tom", introduce: ƒ}
// Tom

const bob = {
  _name: 'Bob'
}

console.log(Reflect.get(tom, 'name', bob));
// this: {_name: "Bob"}
// Bob

この第3引数の仕組みは、Proxy と Reflect を組み合わせて使用する際に必要になる。

第3引数 receiver の使用例

例えば、次のようなオブジェクトを定義する。高校数学で習うベクトルを表現してみた。ベクトルの長さを与えるプロパティ length は、ゲッターを使用している。

const vector = {
  x: 3,
  y: 4,
  z: 5,
  get length() {
    return Math.sqrt(this.x**2 + this.y**2 + this.z**2);
  }
}

console.log('length:', vector.length);
// length: 7.0710678118654755

このように表現される任意のベクトルに対して、その2倍の長さを持つベクトルを作成する関数 doubleVector を作成したい。ただし、作成されるベクトルは proxy とする。

次のような関数 doubleVector_bad を作成してみた。

function doubleVector_bad(vec) {
  return new Proxy(vec, {
    get: function(targetVec, prop) {
      const value = targetVec[prop];
      if (prop === 'x' || prop === 'y' || prop === 'z') {
        return value * 2;
      }
      return value;
    }
  });
}

const doubled_bad = doubleVector_bad(vector);
console.log('x:', doubled_bad.x);
console.log('y:', doubled_bad.y);
console.log('z:', doubled_bad.z);
console.log('length:', doubled_bad.length);
// x: 6
// y: 8
// z: 10
// length: 7.0710678118654755

x, y, z 成分(プロパティ)は取得しようとすると2倍の値が返され、length プロパティはそれらから計算されることを狙ったのだが、上手く行っていない。これは length のゲッター処理の中の this が vector を参照するために起こる不具合だ。生成した proxy から length にアクセスするときは、ゲッターの this はその proxy を参照して欲しい。

これを実現するには、Reflect.get() メソッドの第3引数を利用すればよい。

function doubleVector_good(vec) {
  return new Proxy(vec, {
    get: function(targetVec, prop, receiver) {
      const value = Reflect.get(targetVec, prop, receiver);
      if (prop === 'x' || prop === 'y' || prop === 'z') {
        return value * 2;
      }
      return value;
    }
  });
}

const doubled_good = doubleVector_good(vector);
console.log('x:', doubled_good.x);
console.log('y:', doubled_good.y);
console.log('z:', doubled_good.z);
console.log('length:', doubled_good.length);
// x: 6
// y: 8
// z: 10
// length: 14.142135623730951

タイトルとURLをコピーしました