uhyohyo.net

JavaScript初級者から中級者になろう

十六章第十五回 Reflect

このページの最終更新日:

今回はReflectを紹介します。Reflectは組み込みオブジェクトであり、いくつかのメソッドを持っています。これらのメソッドはオブジェクトの操作に利用することができます。

Reflectが持っているメソッドを列挙すると以下の通りです:get, set, has, getOwnPropertyDescriptor, ownKeys, defineProperty, deleteProperty, preventExtensions, isExtensible, getPrototypeOf, setPrototypeOf, apply, construct。

これを見ると明らかにあれを思い出しますね。そう、前回紹介したばかりのProxyです。実は、Reflectに存在するメソッドはProxyのトラップと一対一で対応しています。そして、Reflectの各メソッドに渡す引数はトラップに渡される引数と全く一緒です。ただしProxyではないので、第1引数(もとのオブジェクト)は今回の場合は対象のオブジェクトそのものとなります。

たとえば、Reflect.getを使ってみましょう。


var obj = {
  foo: 123,
};

// obj.foo と同じ意味
console.log(Reflect.get(obj, 'foo', obj)); // 123

obj.fooとしてobjのfooプロパティを参照したいとしましょう。もしobjがProxyだったとすると、getトラップが第1引数がもとのオブジェクト、第2引数がプロパティ名('foo')、第3引数がobjで呼ばれることになります。よって、それらの引数をReflect.getで再現することにより、obj.fooを取得することができます。

Reflectのメソッドは全てトラップと対応しているので、Proxyのトラップで対応できる操作はReflectのメソッドで再現できます。また、Reflectのメソッドの返り値もトラップのそれと同じです。トラップの場合は成功か失敗かを真偽値で返すメソッドがありましたが、そのような操作の場合はReflectの対応するメソッドの返り値も真偽値となります。

なので、Reflectのメソッドの特徴は、エラーを発生させないということです。

例えば、Reflect.definePropertyメソッドを考えます。これの動作はObject.definePropertyとほとんど同じですが、不正な操作をしようとした場合の挙動が異なります。例として、configurableでないメソッドの設定を変えようとした場合を考えます。


var obj = {};

// プロパティfooを作成
Object.defineProperty(obj, 'foo', {
  value: 'Hey!',
  writable: false,
  configurable: false,
});

// fooをwritableにしようとするとエラー
Object.defineProperty(obj, 'foo', {
  writable: true,
});

この場合、Object.definePropertyの場合はエラーとなります。一方、Reflect.definePropertyを使うとエラーが発生せず、失敗を表すfalseが返り値として返ってきます。


var obj = {};

// プロパティfooを作成
Object.defineProperty(obj, 'foo', {
  value: 'Hey!',
  writable: false,
  configurable: false,
});

// fooをwritableにしようとする (false)
console.log(Reflect.defineProperty(obj, 'foo', {
  writable: true,
}));

このような特徴から、Reflectのメソッドは失敗してもエラーを発生させたくない場合に適しています。また、明らかにProxyとよく対応していることから、Proxyと一緒に使われることがあります。前回出てきたgetトラップの例を思い出してください。


var target = {
  apple: 'りんご',
  orange: 'みかん',
};
var obj = new Proxy(target, {
  get(target, name, receiver){
    if (target.hasOwnProperty(name)){
      return target[name];
    } else {
      return 'hello';
    }
  },
});

console.log(obj.apple); // "りんご"
console.log(obj.peach); // "hello"

obj.peach = 'もも';
console.log(obj.peach); // "もも"

これはプロパティのデフォルト値を持つオブジェクトの例です。getトラップは指定されたプロパティを持っている場合は本来の動作(そのプロパティの値を取得する)をし、そうでない場合はデフォルト値を返しています。このようにトラップから「本来の動作」を行いたいという場面は結構あります。

このような場合にReflectのメソッドを使うと綺麗に書くことができます。Reflectのメソッドに渡す引数はトラップに渡された引数と対応しているので、次のようにできます。


var target = {
  apple: 'りんご',
  orange: 'みかん',
};
var obj = new Proxy(target, {
  get(target, name, receiver){
    if (target.hasOwnProperty(name)){
      return Reflect.get(target, name, receiver);
    } else {
      return 'hello';
    }
  },
});

console.log(obj.apple); // "りんご"
console.log(obj.peach); // "hello"

obj.peach = 'もも';
console.log(obj.peach); // "もも"

これがReflectの典型的な使いみちのひとつです。

receiverの意味

ここからは前回の補足のようになります。上2つの例(target[name]Reflect.get(target, name, receiver))は実は挙動が異なることがあります。挙動の違いは、オブジェクトがゲッタを持つ場合に現れます。


var target = {
  apple: 'りんご',
  orange: 'みかん',
  get foo(){
    return this.peach;
  },
};
var obj = new Proxy(target, {
  get(target, name, receiver){
    if (target.hasOwnProperty(name)){
      return target[name];
    } else {
      return 'hello';
    }
  },
});

console.log(obj.foo); // undefined

この例では、もとのオブジェクト(target)のfooというプロパティがゲッタとなっています。そして、ゲッタの中ではthisを使うことができます。例えばobj.fooとしてプロパティを取得した場合はthisはobjとなります。これはメソッド呼び出しと同じですね。

しかし、今回obj.fooとされると、objはProxyなのでまずgetトラップが処理され、target[name](ここでnameは"foo")の値が返されます。targetのfooプロパティはゲッタとなっているのでゲッタが呼ばれますが、この中でthisはtargetオブジェクトになります。ということは、this.peachとしてもpeachプロパティは存在しないのでundefinedが返ります。

これは、あるいは非直感的な挙動ではないでしょうか。objのfooプロパティを取得しようとしたのだから、thisはobjとなり、したがってthis.peachはデフォルト値の'hello'になるべきだとは思いませんか。

実は、Reflect.getを使えばそのような挙動とすることができます。


var target = {
  apple: 'りんご',
  orange: 'みかん',
  get foo(){
    return this.peach;
  },
};
var obj = new Proxy(target, {
  get(target, name, receiver){
    if (target.hasOwnProperty(name)){
      return Reflect.get(target, name, receiver);
    } else {
      return 'hello';
    }
  },
});

console.log(obj.foo); // "hello"

この秘密はgetトラップの第3引数、receiverにあります。前回の説明では、obj.fooとしたときreceiverにはobjが入ると説明しました。実はreceiverというのは、ゲッタ(あるいはセッタ)内でthisになるべきオブジェクトを表しています。よって、Reflect.getは第3引数に渡されたもの(receiver)をゲッタの呼び出し時のthisとして扱うのです。

これがreceiverの意味です。getトラップやsetトラップの引数のうち最後のもの(receiver)はあまり使う機会がないので忘れがちですが、Reflect.getなどを使う場合は忘れないようにしましょう。

とはいえ、実はReflect.getReflect.setにおいては最後の引数は省略可能です。一番最初の例でReflect.get(obj, 'foo', obj);とするのはobjが2回出てきて無駄ですね。このような場合は第3引数を省略できます(省略すると第1引数と同じになります)。