メタプログラミング

Proxy および Reflect オブジェクトにより、基本的な言語操作 (例えば、プロパティ参照、代入、列挙、関数呼び出しなど) に割り込み、動作をカスタマイズすることができます。この 2 つのオブジェクトのおかげで、 JavaScript でメタレベルのプログラミングが行えます。

プロキシー

Proxy オブジェクトによって、特定の操作に割り込んで動作をカスタマイズすることができます。

例えば、オブジェクトのプロパティを取得してみましょう。

js
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 オブジェクトに対して利用可能なトラップをまとめたものです。詳細な説明と例については、リファレンスページを参照してください。

ハンドラー / トラップ 割り込みされる処理
handler.getPrototypeOf() Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
handler.setPrototypeOf() Object.setPrototypeOf()
Reflect.setPrototypeOf()
handler.isExtensible() Object.isExtensible()
Reflect.isExtensible()
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]
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.ownKeys() Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
handler.apply() proxy(..args)
Function.prototype.apply() および Function.prototype.call()
Reflect.apply()
handler.construct() new proxy(...args)
Reflect.construct()

取り消し可能 Proxy

Proxy.revocable() メソッドは取り消し可能な Proxy オブジェクトの生成に使用されます。これにより、プロキシーを revoke 関数で取り消し、プロキシーの機能を停止することができます。

その後はプロキシーを通じたいかなる操作も TypeError になります。

js
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 演算子を関数として使うことができます。

js
Reflect.has(Object, "assign"); // true

より優れた apply() 関数

Reflect が登場する前は、所定の this 値と配列や配列風オブジェクトとして提供される arguments を使って関数を呼び出す Function.prototype.apply() メソッドがよく使われてきました。

js
Function.prototype.apply.call(Math.floor, undefined, [1.75]);

Reflect.apply を使えば、より簡潔で分かりやすいものにできます。

js
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 ブロックを使うだけでよいのです。

js
if (Reflect.defineProperty(target, property, attributes)) {
  // 成功した時の処理
} else {
  // 失敗した時の処理
}