メタプログラミング

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

プロキシー

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

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

js
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
  • 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]
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 モードでは、 false という返値が set ハンドラーから返された場合、TypeError 例外が発生します。
    handler.deleteProperty()
    プロパティの削除
    delete proxy[foo]
    delete proxy.foo
    Reflect.deleteProperty()
    target に構成不可の所有プロパティとして存在する場合、削除することはできません。
    handler.enumerate()
    プロパティの列挙 / for...in:
    for (let name in proxy) {...}
    Reflect.enumerate()
    enumerate メソッドはオブジェクトを返す必要があります。
    handler.ownKeys() Object.getOwnPropertyNames()
    Object.getOwnPropertySymbols()
    Object.keys()
    Reflect.ownKeys()
    • ownKeys の結果はリストとなります。
    • 出力リストの要素の型は StringSymbol のどちらかとなります。
    • 出力リストは target のすべての設定不可の所有プロパティのキーを含める必要があります。
    • ターゲットオブジェクトが拡張できない場合、出力リストはターゲットオブジェクト中の所有プロパティのキーをすべて含める必要があり、他の値は含まれません。
    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 になります。

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

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

    より優れた apply 関数

    ES5 では、所定の 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 {
      // 失敗した時の処理
    }