概要

Proxy と Reflect は、ES6 がオブジェクトを操作するために導入した API です。

Proxy は、ターゲットオブジェクトの読み取りや関数呼び出しなどの操作をインターセプトし、その後操作処理を行うことができます。オブジェクトを直接操作するのではなく、プロキシパターンのように、オブジェクトのプロキシオブジェクトを介して操作を行います。これらの操作を行う際に、必要な追加操作を加えることができます。

Reflect はターゲットオブジェクトの振る舞いを取得するために使用できます。Object に似ていますが、より読みやすく、オブジェクト操作に、よりエレガントな方法を提供します。そのメソッドは Proxy と対応しています。


基本的な使い方

Proxy

1つの Proxy オブジェクトは、target と handler の2つの部分から構成されます。Proxy コンストラクタを介してインスタンスオブジェクトを生成する際には、この2つのパラメータを提供する必要があります。target はターゲットオブジェクトであり、handler は、ターゲットの指定された振る舞いを代理することを宣言するオブジェクトです。

let target = { name: 'Tom', age: 24 } let handler = { get: function(target, key) { console.log('getting '+key); return target[key]; //target.key ではなく }, set: function(target, key, value) { console.log('setting '+key); target[key] = value; } } let proxy = new Proxy(target, handler) proxy.name //実際には handler.get が実行される proxy.age = 25 //実際には handler.set が実行される // getting name // setting age // 25 //target は空のオブジェクトでもよい let targetEpt = {} let proxyEpt = new Proxy(targetEpt, handler) //get メソッドを呼び出す。このときターゲットオブジェクトは空で、name プロパティはない proxyEpt.name // getting name //set メソッドを呼び出し、ターゲットオブジェクトに name プロパティを追加した proxyEpt.name = 'Tom' // setting name // "Tom" //再度 get を呼び出すと、このとき name プロパティがすでに存在する proxyEpt.name // getting name // "Tom" //コンストラクタを介して新しいインスタンスを作成するときは、実際にはターゲットオブジェクトに対してシャローコピー(浅いコピー)が行われるため、ターゲットオブジェクトとプロキシオブジェクトは互いに //影響する targetEpt // {name: "Tom"} //handler オブジェクトを空にすることもできます。これは、インターセプト操作を設定せずに、ターゲットオブジェクトに直接アクセスすることに相当します。 let targetEmpty = {} let proxyEmpty = new Proxy(targetEmpty,{}) proxyEmpty.name = "Tom" targetEmpty // {name: "Tom"}

インスタンスメソッド

get(target, propKey, receiver)

target オブジェクト上での propKey の読み取り操作に使用されます。

let exam ={ name: "Tom", age: 24 } let proxy = new Proxy(exam, { get(target, propKey, receiver) { console.log('Getting ' + propKey); return target[propKey]; } }) proxy.name // Getting name // "Tom"

get() メソッドは継承できます。

let proxy = new Proxy({}, { get(target, propKey, receiver) { //プライベートプロパティの読み取り保護を実装する if(propKey[0] === '_'){ throw new Erro(`Invalid attempt to get private "${propKey}"`); } console.log('Getting ' + propKey); return target[propKey]; } }); let obj = Object.create(proxy); obj.name // Getting name
set(target, propKey, value, receiver)

target オブジェクト上での propKey の代入操作をインターセプトするために使用されます。ターゲットオブジェクト自身のあるプロパティが書き込み不可かつ設定変更不可である場合、set メソッドは機能しません。

let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } //条件を満たす age プロパティおよびその他のプロパティは、直接保存します obj[prop] = value; } }; let proxy= new Proxy({}, validator) proxy.age = 100; proxy.age // 100 proxy.age = 'oppps' //エラーが発生します proxy.age = 300 //エラーが発生します

4番目のパラメータ receiver は、元の操作の振る舞いが存在するオブジェクトを表し、通常は Proxy インスタンス自体です。

const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; } }; const proxy = new Proxy({}, handler); proxy.name= 'Tom'; proxy.name=== proxy // true const exam = {} Object.setPrototypeOf(exam, proxy) exam.name = "Tom" exam.name === exam // true

注意: 厳格モードでは、set プロキシが true を返さない場合、エラーが発生します。

apply(target, ctx, args)

関数の呼び出し、call および reply 操作をインターセプトするために使用されます。target はターゲットオブジェクト、ctx はターゲットオブジェクトのコンテキスト、args はターゲットオブジェクトの引数配列を表します。

function sub(a, b){ return a - b; } let handler = { apply: function(target, ctx, args){ console.log('handle apply'); return Reflect.apply(...arguments); } } let proxy = new Proxy(sub, handler) proxy(2, 1) // handle apply // 1
has(target, propKey)

HasProperty 操作をインターセプトするために使用されます。つまり、target オブジェクトに propKey プロパティが存在するかどうかを判断する際に、このメソッドによってインターセプトされます。このメソッドは、プロパティがオブジェクト自身のプロパティであるか、継承されたプロパティであるかは判断しません。

let handler = { has: function(target, propKey){ console.log("handle has"); return propKey in target; } } let exam = {name: "Tom"} let proxy = new Proxy(exam, handler) 'name' in proxy // handle has // true

注意: このメソッドは for...in ループをインターセプトしません。

construct(target, args)

new コマンドをインターセプトするために使用されます。戻り値はオブジェクトでなければなりません。

let handler = { construct: function (target, args, newTarget) { console.log('handle construct') return Reflect.construct(target, args, newTarget) } } class Exam { constructor (name) { this.name = name } } let ExamProxy = new Proxy(Exam, handler) let proxyObj = new ExamProxy('Tom') console.log(proxyObj) // handle construct // exam {name: "Tom"}
deleteProperty(target, propKey)

delete 操作をインターセプトするために使用されます。このメソッドがエラーをスローするか false を返すと、propKey プロパティは delete コマンドで削除できません。

defineProperty(target, propKey, propDesc)

Object.definePro をインターセプトするために使用されます。ターゲットオブジェクトが拡張不可の場合、ターゲットオブジェクトに存在しない属性を追加するとエラーになります。属性が書き込み不可または設定変更不可の場合、これらの属性を変更することはできません。

let handler = { defineProperty: function(target, propKey, propDesc){ console.log("handle defineProperty"); return true; } } let target = {} let proxy = new Proxy(target, handler) proxy.name = "Tom" // handle defineProperty target // {name: "Tom"} //defineProperty の戻り値が false の場合、属性の追加操作は無効です。 let handler1 = { defineProperty: function(target, propKey, propDesc){ console.log("handle defineProperty"); return false; } } let target1 = {} let proxy1 = new Proxy(target1, handler1) proxy1.name = "Jerry" target1 // {}

erty 操作

getOwnPropertyDescriptor(target, propKey)

Object.getOwnPropertyD() をインターセプトするために使用されます。戻り値はプロパティ記述オブジェクトまたは undefined です。

let handler = { getOwnPropertyDescriptor: function(target, propKey){ return Object.getOwnPropertyDescriptor(target, propKey); } } let target = {name: "Tom"} let proxy = new Proxy(target, handler) Object.getOwnPropertyDescriptor(proxy, 'name') // {value: "Tom", writable: true, enumerable: true, configurable: // true}

ptor 属性

getPrototypeOf(target)

主にオブジェクトのプロトタイプを取得する操作をインターセプトするために使用されます。以下の操作が含まれます:

- Object.prototype._proto_
- Object.prototype.isPrototypeOf()
- Object.getPrototypeOf()
- Reflect.getPrototypeOf()
- instanceof
let exam = {} let proxy = new Proxy({},{ getPrototypeOf: function(target){ return exam; } }) Object.getPrototypeOf(proxy) // {}

注意: 戻り値はオブジェクトまたは null でなければなりません。そうでない場合はエラーが発生します。また、ターゲットオブジェクトが拡張不可(non-extensible)の場合、getPrototypeOf メソッドはターゲットオブジェクトのプロトタイプオブジェクトを返さなければなりません。

let proxy = new Proxy({},{ getPrototypeOf: function(target){ return true; } }) Object.getPrototypeOf(proxy) // TypeError: 'getPrototypeOf' on proxy: trap returned neither object // nor null
isExtensible(target)

Object.isExtensible 操作をインターセプトするために使用されます。

このメソッドはブール値のみを返すことができます。そうでない場合、戻り値は自動的にブール値に変換されます。

let proxy = new Proxy({},{ isExtensible:function(target){ return true; } }) Object.isExtensible(proxy) // true

注意: その戻り値はターゲットオブジェクトの isExtensible プロパティと一致している必要があります。そうでない場合、エラーがスローされます。

let proxy = new Proxy({},{ isExtensible:function(target){ return false; } }) Object.isExtensible(proxy) // TypeError: 'isExtensible' on proxy: trap result does not reflect // extensibility of proxy target (which is 'true')
ownKeys(target)

オブジェクト自身のプロパティの読み取り操作をインターセプトするために使用されます。主に以下の操作が含まれます:

- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- or...in

メソッドが返す配列のメンバーは、文字列または Symbol 値のみでなければなりません。そうでない場合、エラーが発生します。

ターゲットオブジェクトに設定変更不可のプロパティが含まれている場合、これらのプロパティを結果に返す必要があります。そうでない場合はエラーが発生します。

ターゲットオブジェクトが拡張不可の場合、すべてを返さなければならず、ターゲットオブジェクトが含むすべてのプロパティのみを返すことができ、存在しないプロパティを含めることはできません。そうでない場合もエラーが発生します。

let proxy = new Proxy( { name: "Tom", age: 24 }, { ownKeys(target) { return ['name']; } }); Object.keys(proxy) //['name'] の返却結果では、3種類のプロパティがフィルタリングされます: //- ターゲットオブジェクトに存在しないプロパティ //- プロパティ名が Symbol 値であるプロパティ //- 列挙不可のプロパティ let target = { name: "Tom", [Symbol.for('age')]: 24, }; //列挙不可のプロパティ 'gender' を追加する Object.defineProperty(target, 'gender', { enumerable: false, configurable: true, writable: true, value: 'male' }); let handler = { ownKeys(target) { return ['name', 'parent', Symbol.for('age'), 'gender']; } }; let proxy = new Proxy(target, handler); Object.keys(proxy) // ['name']
preventExtensions(target)

Object.preventExtensions 操作をインターセプトします。

このメソッドはブール値を返さなければなりません。そうでない場合、自動的にブール値に変換されます。

//ターゲットオブジェクトが拡張不可の場合(つまり Object.isExtensible(proxy) が false の場合)のみ、 //proxy.preventExtensions は true を返すことができます。そうでない場合、エラーが発生します。 var proxy = new Proxy({}, { preventExtensions: function(target) { return true; } }); //proxy.preventExtensions が true を返すため、ここでも true が返され、エラーが発生します。 Object.preventExtensions(proxy)される// TypeError: 'preventExtensions' on proxy: trap returned truish but // the proxy target is extensible //解決策 var proxy = new Proxy({}, { preventExtensions: function(target) { //返す前に Object.preventExtensions を呼び出します Object.preventExtensions(target); return true; } }); Object.preventExtensions(proxy) // Proxy {}
setPrototypeOf

主に Object.setPrototypeOf メソッドをインターセプトするために使用されます。

戻り値はブール値でなければなりません。そうでない場合、自動的にブール値に変換されます。

ターゲットオブジェクトが拡張不可の場合、setPrototypeOf メソッドはターゲットオブジェクトのプロトタイプを変更してはなりません。

let proto = {} let proxy = new Proxy(function () {}, { setPrototypeOf: function(target, proto) { console.log("setPrototypeOf"); return true; } } ); Object.setPrototypeOf(proxy, proto); // setPrototypeOf
Proxy.revocable()

キャンセル可能な Proxy インスタンスを返すために使用されます。

let {proxy, revoke} = Proxy.revocable({}, {}); proxy.name = "Tom"; revoke(); proxy.name // TypeError: Cannot perform 'get' on a proxy that has been revoked

Reflect

ES6では、Objectの明らかに言語内部に属するいくつかのメソッドがReflectオブジェクトに移植されました(現在、一部のメソッドはObjectとReflectの両方に存在します)。将来の新しいメソッドはReflectオブジェクトにのみ配置されます。

Reflect オブジェクトは、一部のメソッドの戻り値を変更して、より合理的にしています。

Reflect オブジェクトは、関数方式で Object の命令的操作を実装しています。

静的メソッド

Reflect.get(target, name, receiver)

target オブジェクトの name プロパティを検索して返します。

let exam = { name: "Tom", age: 24, get info(){ return this.name + this.age; } } Reflect.get(exam, 'name'); // "Tom" //target オブジェクトに name プロパティの getter メソッドが存在する場合、getter メソッドの this は // receiver にバインドされます let receiver = { name: "Jerry", age: 20 } Reflect.get(exam, 'info', receiver); // Jerry20 //name が target オブジェクトに存在しないプロパティの場合、undefined を返します Reflect.get(exam, 'birth'); // undefined //target がオブジェクトでない場合、エラーが発生します Reflect.get(1, 'name'); // TypeError
Reflect.set(target, name, value, receiver)

target の name プロパティを value に設定します。戻り値は boolean で、true は変更成功を表し、false は失敗を表します。target がオブジェクトでない場合、エラーが発生します。

let exam = { name: "Tom", age: 24, set info(value){ return this.age = value; } } exam.age; // 24 Reflect.set(exam, 'age', 25); // true exam.age; // 25 //value が空の場合は name プロパティをクリアします Reflect.set(exam, 'age', ); // true exam.age; // undefined //target オブジェクトに name プロパティの setter メソッドが存在する場合、setter メソッド内の this は // receiver にバインドされます。そのため、実際に変更されるのは receiver のプロパティです。 let receiver = { age: 18 } Reflect.set(exam, 'info', 1, receiver); // true receiver.age; // 1 let receiver1 = { name: 'oppps' } Reflect.set(exam, 'info', 1, receiver1); receiver1.age; // 1
Reflect.has(obj, name)

name in obj 命令を関数化したもので、name プロパティが obj オブジェクト内に存在するかどうかを調べるために使用されます。戻り値は boolean です。obj がオブジェクトでない場合は TypeError が発生します。

let exam = { name: "Tom", age: 24 } Reflect.has(exam, 'name'); // true
Reflect.deleteProperty(obj, property)

delete obj[property] を関数化したもので、obj オブジェクトの property プロパティを削除するために使用されます。戻り値は boolean です。obj がオブジェクトでない場合は TypeError が発生します。

let exam = { name: "Tom", age: 24 } Reflect.deleteProperty(exam , 'name'); // true exam // {age: 24} //property が存在しない場合も true を返します Reflect.deleteProperty(exam , 'name'); // true
Reflect.construct(obj, args)

new target(...args) と同等です。

function exam(name){ this.name = name; } Reflect.construct(exam, ['Tom']); // exam {name: "Tom"}
Reflect.getPrototypeOf(obj)

obj の _proto_ プロパティを読み取るために使用されます。obj がオブジェクトでない場合、Object のように obj をオブジェクトに変換するのではなく、エラーが発生します。

class Exam{} let obj = new Exam() Reflect.getPrototypeOf(obj) === Exam.prototype // true
Reflect.setPrototypeOf(obj, newProto)

ターゲットオブジェクトの prototype を設定するために使用されます。

let obj ={} Reflect.setPrototypeOf(obj, Array.prototype); // true
Reflect.apply(func, thisArg, args)

Function.prototype.apply.call(func, thisArg, args) と同等です。func はターゲット関数を表します。thisArg はターゲット関数にバインドされる this オブジェクトを表します。args はターゲット関数呼び出し時に渡される引数リストで、配列または配列風オブジェクトです。ターゲット関数が呼び出し可能でない場合、TypeError がスローされます。

Reflect.apply(Math.max, Math, [1, 3, 5, 3, 1]); // 5
Reflect.defineProperty(target, propertyKey, attributes)

対象オブジェクトにプロパティを定義するために使用します。target がオブジェクトでない場合、エラーがスローされます。

let myDate= {} Reflect.defineProperty(MyDate, 'now', { value: () => Date.now() }); // true const student = {}; Reflect.defineProperty(student, "name", {value: "Mike"}); // true student.name; // "Mike"
Reflect.getOwnPropertyDescriptor(target, propertyKey)

target オブジェクトの propertyKey プロパティの記述子を取得するために使用します。target がオブジェクトでない場合、引数が不正であることを示すエラーがスローされ、非オブジェクトをオブジェクトに変換しません。

var exam = {} Reflect.defineProperty(exam, 'name', { value: true, enumerable: false, }) Reflect.getOwnPropertyDescriptor(exam, 'name') // { configurable: false, enumerable: false, value: true, writable: // false} //propertyKey プロパティが target オブジェクト内に存在しない場合、undefined を返します。 Reflect.getOwnPropertyDescriptor(exam, 'age') // undefined
Reflect.isExtensible(target)

target オブジェクトが拡張可能かどうかを判定するために使用します。戻り値は boolean です。target 引数がオブジェクトでない場合、エラーがスローされます。

let exam = {} Reflect.isExtensible(exam) // true
Reflect.preventExtensions(target)

target オブジェクトを拡張不可にするために使用します。target 引数がオブジェクトでない場合、エラーがスローされます。

let exam = {} Reflect.preventExtensions(exam) // true
Reflect.ownKeys(target)

target オブジェクトのすべてのプロパティを返すために使用します。これは Object.getOwnPropertyNames と Object.getOwnPropertySymbols を合わせたものに相当します。

var exam = { name: 1, [Symbol.for('age')]: 4 } Reflect.ownKeys(exam) // ["name", Symbol(age)]

組み合わせて使用

Reflect オブジェクトのメソッドと Proxy オブジェクトのメソッドは一対一対応です。したがって、Proxy オブジェクトのメソッドは Reflect オブジェクトのメソッドを呼び出すことでデフォルトの動作を取得し、その後追加の操作を行うことができます。

let exam = { name: "Tom", age: 24 } let handler = { get: function(target, key){ console.log("getting "+key); return Reflect.get(target,key); }, set: function(target, key, value){ console.log("setting "+key+" to "+value) Reflect.set(target, key, value); } } let proxy = new Proxy(exam, handler) proxy.name = "Jerry" proxy.name // setting name to Jerry // getting name // "Jerry"

使用シーンの拡張

オブザーバーパターンの実装

//Set 集合を定義する const queuedObservers = new Set(); //観察者関数をすべて Set 集合に入れます。 const observe = fn => queuedObservers.add(fn); //observable は元のオブジェクトのプロキシを返し、代入操作をインターセプトします。 const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { //オブジェクトへの代入操作を取得します。 const result = Reflect.set(target, key, value, receiver); //すべての観察者を実行します。 queuedObservers.forEach(observer => observer()); //代入操作を実行します。 return result; }