メタプログラミング
ECMAScript 2015 から、JavaScript には Proxy
オブジェクトと Reflect
オブジェクトがサポートされました。これらは基本的な言語操作 (例えば、プロパティ参照、代入、列挙、関数呼び出しなど) に割り込み、動作をカスタマイズすることができます。この 2 つのオブジェクトのおかげで、JavaScript でメタレベルのプログラミングが行えます。
プロキシー
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
(ここでは空オブジェクト) と handler
オブジェクトを定義し、その中に get
トラップが実装されています。ここで、プロキシーとなったオブジェクトは未定義のプロパティを取得しようとした時に 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() |
|
|
handler.get() |
|
|
handler.set() |
|
|
handler.deleteProperty() |
|
target
に構成不可の所有プロパティとして存在する場合、削除することはできません。
|
handler.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"
プロパティ定義の成否チェック
Object.defineProperty
は成功すればオブジェクトを返し、そうでなければ TypeError
を投げるので、 try...catch
ブロックを使って、プロパティの定義中に発生したエラーを捉えます。 Reflect.defineProperty
は成功のステータスを論理値で返すので、ここでは if...else
ブロックを使うだけでよいのです。
if (Reflect.defineProperty(target, property, attributes)) {
// 成功した時の処理
} else {
// 失敗した時の処理
}