メタプログラミング
Proxy
および Reflect
オブジェクトにより、基本的な言語操作 (例えば、プロパティ参照、代入、列挙、関数呼び出しなど) に割り込み、動作をカスタマイズすることができます。この 2 つのオブジェクトのおかげで、 JavaScript でメタレベルのプログラミングが行えます。
プロキシー
Proxy
オブジェクトによって、特定の操作に割り込んで動作をカスタマイズすることができます。
例えば、オブジェクトのプロパティを取得してみましょう。
const handler = {
get(target, name) {
return name in target ? target[name] : 42;
},
};
const 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
オブジェクトに対して利用可能なトラップをまとめたものです。詳細な説明と例については、リファレンスページを参照してください。
取り消し可能 Proxy
Proxy.revocable()
メソッドは取り消し可能な Proxy
オブジェクトの生成に使用されます。これにより、プロキシーを revoke
関数で取り消し、プロキシーの機能を停止することができます。
その後はプロキシーを通じたいかなる操作も TypeError
になります。
const revocable = Proxy.revocable(
{},
{
get(target, name) {
return `[[${name}]]`;
},
},
);
const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"
revocable.revoke();
console.log(proxy.foo); // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1; // TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy); // "object" が返され, typeof はどんなトラップも引き起こさない
リフレクション
Reflect
は JavaScript で割り込み操作を行うメソッドを提供する組み込みオブジェクトです。そのメソッドはプロキシーのハンドラーのメソッドと同じです。
Reflect
は関数オブジェクトではありません。
Reflect
はハンドラーからターゲット
への既定の操作を転送するのに役立ちます。
例えば、Reflect.has()
を使えば、 in
演算子を関数として使うことができます。
Reflect.has(Object, "assign"); // true
より優れた apply() 関数
Reflect
が登場する前は、所定の 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 {
// 失敗した時の処理
}