メタプログラミング

この記事は技術レビューを必要としています。ぜひご協力ください

この記事は編集レビューを必要としています。ぜひご協力ください

ECMAScript 6 から、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
  • getPrototypeOf メソッドはオブジェクトか null を返す必要があります。
  • target が拡張不可の場合、Object.getPrototypeOf(proxy) メソッドは Object.getPrototypeOf(target) と同じ値を返す必要があります。
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()

Object.isExtensible(proxy)false の場合のみ、Object.preventExtensions(proxy)true を返します。

handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()
  • getOwnPropertyDescriptor はオブジェクトか undefined のいずれかを返す必要があります。
  • ターゲットオブジェクトに設定不可の所有プロパティとして存在する場合、そのプロパティについて存在しないと報告することはできません。
  • 拡張不可のターゲットオブジェクトに所有プロパティとして存在する場合、そのプロパティについて存在しないと報告することはできません。
  • 拡張不可のターゲットオブジェクトに所有プロパティとして存在しない場合、そのプロパティについて存在すると報告することはできません。
  • ターゲットオブジェクトに所有プロパティとして存在しない場合、あるいはターゲットオブジェクトに設定可能な所有プロパティとして存在する場合、そのプロパティについて設定不可と報告することはできません。
  • Object.getOwnPropertyDescriptor(target) の結果は Object.defineProperty を使用してターゲットオブジェクトに適用され、この時に例外は発生しません。
handler.defineProperty() Object.defineProperty()
Reflect.defineProperty()
  • ターゲットオブジェクトが拡張可能ではない場合、プロパティは追加できません。
  • ターゲットオブジェクトに設定不可の所有プロパティとして存在しない場合、そのプロパティを追加したり、また設定不可に更新することはできません。
  • ターゲットオブジェクトに対応する設定可能なプロパティとして存在する場合、そのプロパティを設定不可としてもかまいません。
  • プロパティが対応するターゲットオブジェクトプロパティを持つ場合、Object.defineProperty(target, prop, descriptor) は例外を発生しません。
  • strict モードでは、definePropertyハンドラからの戻り値が false の場合、TypeError 例外が発生します。
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()
  • ターゲットオブジェクトプロパティが書込不可、設定不可のデータプロパティである場合、プロパティに対して報告する値は対応するプロパティと同じ値である必要があります。
  • 対応するターゲットオブジェクトプロパティが、Get 属性に undefined を持つ設定不可のアクセサプロパティである場合、プロパティに対して報告される値を undefined とする必要があります。
handler.set() プロパティへの代入 :
proxy[foo] = bar, proxy.foo = bar
継承されたプロパティの割り当て :
Object.create(proxy)[foo] = bar
Reflect.set()
  • 対応するターゲットオブジェクトのプロパティが書込不可、設定不可のデータプロパティである場合、そのプロパティとは違うプロパティ値に変更することはできません。
  • 対応するターゲットオブジェクトプロパティが、Set 属性に undefined を持つ設定不可のアクセサプロパティである場合、プロパティの値を設定することはできません。
  • strict モードでは、set ハンドラからの戻り値が false の場合、TypeError 例外が発生します。
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()
  • ownKeys の結果はリストとなります。
  • 出力リストの要素の型は StringSymbol のどちらかとなります。
  • 出力リストはターゲットオブジェクト中にあるすべての設定不可な所有プロパティのキーを含める必要があります。
  • ターゲットオブジェクトが拡張不可の場合、出力リストはターゲットオブジェクト中の所有プロパティのキーをすべて含める必要があり、他の値は含まれません。
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 {
  // 失敗した時の処理
}

ドキュメントのタグと貢献者

 このページの貢献者: x2357, shide55
 最終更新者: x2357,