ECMAScript 2015 から、JavaScript には Proxy オブジェクトと Reflect オブジェクトがサポートされました。これらは基本的な言語操作(例えば、プロパティ検索、代入、列挙、関数呼び出しなど)に割り込み、動作をカスタマイズできます。この 2 つのオブジェクトのおかげで、JavaScript でメタレベルのプログラミングが行えます。
Proxy
ECMAScript 6 で導入された Proxy オブジェクトによって、特定の操作に割り込んで動作をカスタマイズできます。例えば、オブジェクトのプロパティを取得してみましょう :
var handler = {
get: function(target, name) {
return name in target ? target[name] : 42;
}
};
var p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42
この Proxy オブジェクトは target(ここでは空オブジェクト)と get トラップが実装された handler オブジェクトを定義しています。ここで、プロキシとなったオブジェクトは未定義のプロパティを取得しようとした時 undefined を返さず、代わりに数値 42 を返します。
さらなる使用例がリファレンスの「Proxy」ページにあります。
用語集
プロキシの機能について話題にする際は、次の用語が使用されます。
- ハンドラ (handler)
- トラップを入れるためのプレースホルダ用オブジェクト。
- トラップ (trap)
- プロパティへのアクセスを提供するメソッド。オペレーティングシステムにおけるトラップの概念と同じようなものです。
- ターゲット (target)
- プロキシが仮想化するオブジェクト。これはプロキシのストレージバックエンドとしてしばしば使われます。拡張・設定可能でないプロパティを持つオブジェクトに関する不変条件(変更されないセマンティック、つまりオブジェクトの意味情報)は、このターゲットに対して検証されます。
- 不変条件 (invariant)
- カスタマイズした動作を実装する際、変更されないセマンティックを不変条件と呼びます。ハンドラの不変条件に違反した場合、
TypeErrorが発生します。
ハンドラとトラップ
次の表は、Proxy オブジェクトに対して利用可能なトラップをまとめたものです。詳細と例についてはリファレンスのハンドラについてのページをご覧ください。
| ハンドラ / トラップ | 割り込みされる処理 | 不変条件 |
|---|---|---|
handler.getPrototypeOf() |
Object.getPrototypeOf()Reflect.getPrototypeOf()__proto__Object.prototype.isPrototypeOf()instanceof |
|
handler.setPrototypeOf() |
Object.setPrototypeOf()Reflect.setPrototypeOf() |
target が拡張不可の場合、prototype パラメータは Object.getPrototypeOf(target) と同じ値である必要があります。 |
handler.isExtensible() |
Object.isExtensible()Reflect.isExtensible() |
Object.isExtensible(proxy) は Object.isExtensible(target) と同じ値を返す必要があります。 |
handler.preventExtensions() |
Object.preventExtensions()Reflect.preventExtensions() |
|
handler.getOwnPropertyDescriptor() |
Object.getOwnPropertyDescriptor()Reflect.getOwnPropertyDescriptor() |
|
handler.defineProperty() |
Object.defineProperty()Reflect.defineProperty() |
|
handler.has() |
プロパティの照会 :foo in proxy継承されたプロパティの照会 : foo in Object.create(proxy)Reflect.has() |
|
handler.get() |
プロパティへのアクセス :proxy[foo]and proxy.bar継承されたプロパティアクセス : Object.create(proxy)[foo]Reflect.get() |
|
handler.set() |
プロパティへの代入 :proxy[foo] = bar, proxy.foo = bar継承されたプロパティの割り当て : Object.create(proxy)[foo] = barReflect.set() |
|
handler.deleteProperty() |
プロパティの削除 :delete proxy[foo], delete proxy.fooReflect.deleteProperty() |
ターゲットオブジェクトに設定不可の所有プロパティとして存在する場合、削除することはできません。 |
handler.enumerate() |
プロパティの列挙 / for...in :for (var name in proxy) {...}Reflect.enumerate() |
enumerate メソッドはオブジェクトを返す必要があります。 |
handler.ownKeys() |
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()Reflect.ownKeys() |
|
handler.apply() |
proxy(..args)Function.prototype.apply() and Function.prototype.call()Reflect.apply() |
handler.apply メソッドに対する不変条件はありません。 |
handler.construct() |
new proxy(...args)Reflect.construct() |
出力結果は Object とする必要があります。 |
取り消し可能 Proxy
Proxy.revocable() メソッドは取り消し可能な Proxy オブジェクトの生成に使用されます。これにより、プロキシを revoke 関数で取り消し、プロキシの機能を停止することができます。その後はプロキシを通じたいかなる操作も TypeError になります。
var revocable = Proxy.revocable({}, {
get: function(target, name) {
return "[[" + name + "]]";
}
});
var proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"
revocable.revoke();
console.log(proxy.foo); // TypeError が発生
proxy.foo = 1 // 再び TypeError
delete proxy.foo; // ここでも TypeError
typeof proxy; // "object" が返され, typeof はどんなトラップも引き起こさない
リフレクション
Reflect は JavaScript で割り込み操作を行うメソッドを提供するビルトインオブジェクトです。そのメソッドは Proxy ハンドラのメソッドと同じです。Reflect は関数オブジェクトではありません。
Reflect はハンドラからターゲットへのデフォルト操作を転送するのに役立ちます。
例えば、Reflect.has() を使えば、 in 演算子を関数として使うことができます :
Reflect.has(Object, "assign"); // true
より優れた apply 関数
ES5 では、所定の this 値と配列や配列様のオブジェクトとして提供される arguments を使って関数を呼び出す Function.prototype.apply() メソッドがよく使われてきました。
Function.prototype.apply.call(Math.floor, undefined, [1.75]);
Reflect.apply を使えば、より簡潔で分かりやすいものにできます :
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;
Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"
Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4
Reflect.apply("".charAt, "ponies", [3]);
// "i"
プロパティ定義の成否チェック
成功した時はオブジェクトを返し、失敗した時は TypeError を発生させる Object.defineProperty では、プロパティを定義する際に発生するエラーを捉えるのに try...catch ブロックを使おうとしていたでしょう。Reflect.defineProperty では成功したかどうかによって真偽値を返すので、ここでは if...else ブロックを使えます :
if (Reflect.defineProperty(target, property, attributes)) {
// 成功した時の処理
} else {
// 失敗した時の処理
}