ECMAScript 2015 から、JavaScript には Proxy
オブジェクトと Reflect
オブジェクトがサポートされました。これらは基本的な言語操作(例えば、プロパティ検索、代入、列挙、関数呼び出しなど)に割り込み、動作をカスタマイズできます。この 2 つのオブジェクトのおかげで、JavaScript でメタレベルのプログラミングが行えます。
Proxy
ECMAScript 6 で導入された Proxy
オブジェクトによって、特定の操作に割り込んで動作をカスタマイズできます。
例えば、オブジェクトのプロパティを取得してみましょう :
let handler = {
get: function(target, name) {
return name in target? target[name] : 42
}
}
let 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] = bar Reflect.set() |
|
handler.deleteProperty() |
プロパティの削除 :delete proxy[foo] , delete proxy.foo Reflect.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
になります。
let revocable = Proxy.revocable({}, {
get: function(target, name) {
return '[[' + name + ']]'
}
})
let 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 {
// 失敗した時の処理
}