メタプログラミング
=========

* « 前のページ
* 次のページ »

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` の結果はリストとなります。
* 出力リストの要素の型は `String` か
 `Symbol` のどちらかとなります。
* 出力リストは
 `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 {
  // 失敗した時の処理
}
```

* « 前のページ
* 次のページ »