ES6で新に追加されたProxyについて、その概念や仕組みについての基礎編と、 様々な状況でのProxyの使い方についての活用編に分けてあります。
この基礎編は、Proxyの仕組み全体についてまとめたものです。 細部はECMAScriptの仕様文書でのランタイム仕様に基づいていて、 まったく手軽なものではありません。 むしろ、コード例で応用の仕方をまとめた活用編のほうが、 Proxyのもつ意味について認識しやすいかもしれません。
この文書を書くにあたって、Proxyについて自ら調査し、 Proxyの視点で単一の文書としてまとめていきました。 Proxyを調べていく時点で判明したのは、 ProxyというのはECMAScript内部のランタイム仕様に強く関わるものであり、 きちんと扱うには横断した仕様の解釈が必要となることです。 このために、(どのランタイム実装でも実装されてもいないmodulesを含めて) その他のES6機能にくらべて、文書が極端に少ない状況にあるのだと思われます。
Proxy
は、ES6(ECMAScript2015)より追加されたビルトインオブジェクトです。
ブラウザ固有拡張にあった__proto__
や__noSuchMethod__
等による 動的な
オブジェクト実装の機能 を、標準仕様 として可能にするものです。
Promise
と違い、その実現には実行環境の支援が必要なしくみであり、
babel等のtranspilerやJavaScript実装の Polyfillでは、
完全なサポートができない ものです。
また、標準に組み込まれることでPromise
がJavaScriptによる
非同期プログラミングの標準モデルとなりつつあるように、
Proxy.revocable
が標準に組み込まれている点から、
Proxy
を用いた Object Capability によるアクセス制御モデルが
JavaScriptでの標準となっていくのかもしれません。
まずproxy = new Proxy(target, handler)
の式で、Proxyオブジェクトが作られます。
ここで、以下の要素とその関係を軽くまとめておきます:
-
インスタンス: ここでは値ではないオブジェクト
null
以外でtypeof
で"object"
,"function"
になるもの全般"boolean"
、"number"
、"string"
、"undefined"
、"symbol"
は値
-
Proxy :
new Proxy
で生成されるインスタンス- 実行環境によるサポートが必然である例として、
new Proxy({}, {}) instanceof Proxy
は必ずfalse
になります
- 実行環境によるサポートが必然である例として、
-
Target: Proxyのデフォルト実行の対象になるインスタンス
typeof
演算子等でtarget
の属性が用いられますproxy === target
は必ずfalse
になります
-
Handler: Proxyインスタンスに対する各種インスタンスアクセスへのTrap群を 1つにまとめたインスタンス
- Trapの名前は、Handler上でのプロパティ名になります
-
Trap: 各インスタンスアクセスに対応した関数(
function
インスタンス)handler.has(target, name) {...}
など
例として、console.log(proxy.member)
という式が実行されると、
Trapのhandler.get(target, "member", proxy)
として呼び出され、
このProxyを作るときに与えたHandler上のTrap handler.get()
のコードが実行され、
その結果(戻り値、例外)が上記のproxy.member
式の評価値へ反映される、
という関係になっています。
Proxy
とともにES6で追加されたビルトインオブジェクトReflect
は、
"Esential Internal Method"と呼ばれるエンジンの内部機能に
対応した13の関数を持つオブジェクトです。
Reflect.isExtensible(obj)
=>[[IsExtensible]]()
Reflect.preventExtensions(obj)
=>[[PreventExtensions]]()
Reflect.getPrototypeOf(obj)
=>[[GetPrototypeOf]]()
Reflect.setPrototypeOf(obj, proto)
=>[[SetPrototypeOf]](V)
Reflect.getOwnPropertyDescriptor(obj, name)
=>[[GetOwnProperty]](P)
Reflect.defineProperty(obj, name, desc)
=>[[DefineOwnProperty]](P, Desc)
Reflect.get(obj, name, receiver)
=>[[Get]](P, Rceiver)
Reflect.set(obj, name, value, receiver)
=>[[Set]](P, V, Receiver)
Reflect.has(obj, name)
=>[[HasProperty]](P)
Reflect.onwKeys(obj)
=>[[OwnPropertyKeys]]()
Reflect.apply(func, thisArg, args)
=>[[Call]](thisArgument, argumentsList)
Reflect.construct(func, args, newTarget)
=>[[Construct]](argumentList, newTarget)
Essential Internal Methodはstringなどの値にも存在しますが、
Reflect
の関数はインスタンスに対してのみ利用が可能です。
ProxyでTrapが存在しないHandlerのデフォルトは、
Targetに対してそのTrapに対応するEssential Internal Methodを実行するものです。
よってReflect
は、
ProxyのデフォルトのTrap実装 を集めたオブジェクトとも言えます。
- すなわち
new Proxy({}, {})
とnew Proxy({}, Reflect)
は、 すべて同じProxy処理を行います
メタプログラミングとしてES5追加のObject
付属関数が有用であるように、
Proxy
抜きであってもReflect
単独で有用です。
Reflect
の関数は、 ES5でのObject
に付随したメタプログラミング用関数群と似ているが、 細かい点に違いがあります- 違いとしては、失敗時に、エラーを投げるか、戻り値でfalseを返すか、
ES6の
Symbol
も扱う、など Object.getOwnPropertyNames(obj)
とReflect.ownKeys(obj)
、Object.defineProperty(obj, name, desc)
とReflect.defineProperty(obj, name, desc)
など
- 違いとしては、失敗時に、エラーを投げるか、戻り値でfalseを返すか、
ES6の
ES6新規の構造にも対応している点から、
ES6環境であればObject
付属の関数から、Reflect
付属の同種の関数に
置き換えるのもよいでしょう。
また、func.apply()
は、内部で[[Call]]
を呼び出す実装のメソッドであり
インスタンスで上書きすることも可能です。Reflect.apply(func)
を使えば、
上書きされていても直接的に[[Call]]
を起動できます。
ProxyのTrapでは、ただtargetをReflect
経由で各デフォルト処理へ
転送する以外にも、差し替えや仮想的な属性があるかのように見せかけるなどの
様々なことが可能です。
ただしProxyのTrapでは、どんなことでも可能になるわけではなく、 Target側で制約が設定される ことによって、 Trapで可能な結果が制限される ようになっています。
以下は、その制約の元になる情報です:
-
Object.isExtensibile(target)
: これがfalse
のとき、Trap実行では、target
に対するデフォルトの結果と くらべて違いが出るような結果を返すことができない- ES5で追加されたオブジェクトの属性変更を制限するビルトイン
関数
Object.freeze(o)
、Object.seal(o)
、Object.preventExtensions(o)
のいずれかが適用されると、Object.isExtensible(o)
はfalse
になる
- ES5で追加されたオブジェクトの属性変更を制限するビルトイン
関数
-
desc.configurable
:Object.defineProperty
等で各プロパティに設定する プロパティデスクリプタでのメンバーのconfigurable
がfalse
のとき、 プロパティに関するTrapでは、その名前のプロパティに対してはtarget
に対する 結果とくらべて違いが出る結果を返すことができない- ES5の時点で、
configurable
がfalse
の時、同一オブジェクトだけでなく、 そのオブジェクトをプロトタイプとして持つオブジェクトに対してもObject.defineProperty()
等ができなくなる - この考えが、TargetとProxyのあいだの関係にも拡大された感じ
- ES5の時点で、
-
desc.writable
: プロパティデスクリプタのwritable
がfalse
のとき、get
Trapとset
Trapでは、その名前のプロパティに対してはtarget
に対する結果とくらべて違いが出るような結果を返すことができないfalse
のとき、get
Trapでtarget
での値と違う値は返せないfalse
のとき、set
Trapでfalse
を返さなくてはいけない
Trapでこれらの制約に違反した結果を返す実装をすると、
Trapが発動する操作がProxyに対して行われたとき、
(strict modeいかんにかかわらず)ProxyからTypeError
が発生します。
ECMAScriptにおいてTypeError
とは、実行時に、
各構文やビルトイン関数と、そこで扱う値とのあいだで、
なんらかの不整合がある場合全般で発生する例外です。
各Essential Internal Methodにおける具体的な制約の条件は、 ECMAScript仕様では、 "6.1.7.3 Invariants of the Essential Internal Methods" および "9.5 Proxy Object Internal Methods and Internal Slots" に記載されています。
注意したいのは、デフォルトTrap実装がそうであるというだけであり、
Trap実装の中でアクセス対象にするインスタンスを
new Proxy
でのtarget
として設定する必然性はあまりありません。
制約からの制限を外したければ、target
には{}
やfunction () {}
など、
同種で無制約のままのインスタンスを渡し、各Trapでtarget
は無視して、
制約されたオブジェクトに対してアクセスさせればよいだけです。
このため、このTargetの制約の継承は、 制約設定をProxyで活用するために活用することになるのではないしょうか。
たとえば、Object.keys(obj)
がどうProxyに影響するかを調べましょう。
Object.keys
は、仕様では、"19.1.2.14 Object.keys ( O )"にあります。
その中で"7.3.21 EnumerableOwnNames (O)"が参照され、
そこの2でEssential Internal MethodのO.[[OwnPropertyKeys]]()
が参照されており、
ProxyからownKeys
Trapが呼び出されます。
そして、この下4にO.[[GetOwnProperty]](key)
があることを忘れてはいけません。
ここでProxyからgetOwnPropertyDescriptor
Trapが呼び出され、
その結果がundefined
でなく、enumerable
がtrue
である場合に
key
が結果であるnames
リストに追加される、と書いてあります。
5では、names
の順序を、"13.7.5.15 EnumerateObjectProperties (O)"での順序
と同じにすることが記載されています。
ここから、Object.keys
に作用させるProxyを作るには、
ownKeys
Trapでリストを返すだけでなく、getOwnPropertyDescriptor
Trapで
リスト中の名前に対してenumerable
がtrue
になるプロパティデスクリプタを
返す必要もあります。
そして、さらに呼び出されるProxyの各Trapの条件もチェックします。
"9.5.11 OwnPropertyKeys ( )" のNOTEに条件がまとめられています。
- Targetの各
key
でのプロパティデスクリプタのconfigurable
がfalse
なとき、Trapの結果にそのkey
が含まれていなければTypeError
IsExtensible(target)
がfalse
の場合、Targetのプロパティリストと Trapの結果との間に過不足があったらTypeError
また、"9.5.5 GetOwnProperty (P)"のNOTEを見ると、
- Targetのデスクリプタが
configurable
がfalse
の場合、undefined
を返すとTypeError
IsExtensible(target)
がfalse
の場合、Targetのデスクリプタが存在する場合undefined
を返すとTypeError
IsExtensible(target)
がfalse
の場合、Targetのデスクリプタが存在しない場合、 デスクリプタを返すとTypeError
- Targetのデスクリプタが存在しないか、
デスクリプタの
configurable
がtrue
の場合、configurable
がfalse
のデスクリプタを返すとTypeError
つまり、enumerable
がtrue
なデスクリプタを返すには、
この最後の条件も満たす必要があるのです。
最終的にこれらをまとめると、Object.keys(proxy)
でObject.keys(target)
に存在しないプロパティを返すためには、
以下の3条件を満たすようTrapを実装する必要があることが判明します:
Reflect.isExtensible(target)
がtrue
であることownKeys
Trapで返す各プロパティについて、Targetのデスクリプタが 存在してる場合に、そのconfigurable
がfalse
でないことownKeys
Trapで返す各プロパティについて、getOwnPropertyDescriptor
Trapでは以下の内容のデスクリプタを返すことenumerable
がtrue
であることconfigurable
がtrue
であること
このように、Proxyに対する仕様の読み方としては、 ProxyのTrapが発動するビルトインや構文の"Runtime Semantics"を起点に、 内部仕様をたぐってくことになります。
また、上記でenumerable
がtrue
のデスクリプタを返すためには
別の条件からconfigurable
もtrue
にしなくてはいけないように、
複数の仕様から導かれる条件が生まれることにも注意する必要があります。
以下のコードは、Handlerインスタンスの全13 Trapを、 5カテゴリに分けて列挙したものです:
const handler = {
//[define proxy extensibility]
isExtensible(target) {//=> bool (extensible or not)
return Reflect.isExtensible(target);
},
preventExtensions(target) {//=> bool (block extending or not)
return Reflect.preventExtensions(target);
},
//[access target.constructor.prototype]
getPrototypeOf(target) {//=> prototype object
return Reflect.getPrototypeOf(target);
},
setPrototypeOf(target, proto) {//=> bool (success or not)
return Reflect.setPrototypeOf(target, proto);
},
//[configure properties]
getOwnPropertyDescriptor(target, name) {//=> descriptor object (or undefined)
return Reflect.getOwnPropertyDescriptor(target, name);
},
defineProperty(target, name, desc) {//=> bool (success or not)
return Reflect.defineProperty(target, name, desc);
},
//[basic property access]
get(target, name, receiver) {//=> value
return Reflect.get(target, name, receiver);
},
set(target, name, value, receiver) {//=> bool (success or not)
return Reflect.set(target, name, value, receiver);
},
deleteProperty(target, name) {//=> bool (success or not)
return Reflect.deleteProperty(target, name);
},
has(target, name) {//=> bool
return Reflect.has(target, name);
},
ownKeys(target) {//=> [name]
return Reflect.ownKeys(target);
},
//[proxy as functions] (required `typeof target === "function"`)
apply(target, thisArg, args) {//=> value (result as function)
return Reflect.apply(target, thisArg, args);
},
construct(target, args, newTarget) {//=> object instance
return Reflect.construct(target, args, protoCtor);
},
};
以下は、各Trapと、それが発動する代表的な式や構文との関係です:
-
handler.isExtensible(target)
Object.isExtensible(proxy)
Object.isSealed(proxy)
Object.isFrozen(proxy)
-
handler.preventExtensions(target)
Object.preventExtensions(proxy)
Object.seal(proxy)
Object.freeze(proxy)
-
handler.getPrototypeOf(target)
proxy instanceof Constructor
Object.getPrototypeOf(proxy)
proxy.__proto__
,proxy.valueOf()
, (handler.get
で差し替えてない場合)Object.create(proxy)
、Object.assign(proxy)
-
handler.setPrototypeOf(target, proto)
Object.setPrototypeOf(proxy, proto)
proxy.__proto__ = proto
(handler.set
で差し替えてない場合)
-
handler.getOwnPropertyDescriptor(target, name)
Object.getOwnPropertyDescriptor(proxy, name)
proxy.valueOf()
, (handler.get
で差し替えてない場合)Object.create(proxy)
、Object.assign(proxy)
-
handler.defineProperty(target, name, desc)
Object.defineProperty(proxy, name, desc)
Object.defineProperties(proxy, descs)
-
handler.get(target, name, receiver)
proxy.name
,proxy[name]
,proxy.name()
,proxy[name]()
(および、属性参照をするビルトイン:Object.assign({}, proxy)
, ...)- (destructuring)
let {name} = proxy;
proxy.valueOf()
,proxy[Symbol.toPrimitive]
を内部で駆動する各種の演算子:+proxy
,proxy == true
,proxy * 10
, ...
-
handler.set(target, name, value, receiver)
proxy.name = value
,proxy[name] = value
(および、属性代入をするビルトイン:Object.assign(proxy, {foo:1})
, ...)
-
handler.deleteProperty(target, name)
delete proxy.name
,delete proxy[name]
Object.deleteProperty(proxy, name)
-
handler.has(target, name)
name in proxy
-
handler.ownKeys(target)
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
for (name in proxy) {...}
(注: 要デスクリプタでenumerable
)Object.keys(proxy)
(注: 要デスクリプタでenumerable
)
-
handler.apply(target, thisArg, args)
proxy(...args)
({m: proxy}).m(...args)
proxy.apply(thisArg, args)
proxy.call(thisArg, ...args)
-
handler.construct(target, args, newTarget)
new proxy(...args)
また、Reflect
の同名関数をProxyに対して呼び出すと、
第二引数以降が同じ値となって、Trapが発動します。
ただし、デフォルト値は、Reflect
の関数呼び出し時でのデフォルト値が
渡ります。
Reflect.get(proxy, name, value, obj)
=>handler.get(target, name, value, obj)
Reflect.get(proxy, name, value)
=>handler.get(target, name, value, proxy)
Reflect.construct(proxy, [], func)
=>handler.construct(target, [], func)
Reflect.construct(proxy, [])
=>handler.construct(target, [], proxy)
そして以下の式に対しては、ProxyからTrapは発動しません:
typeof proxy
typeof target
の結果がそのまま使われる
proxy === value
,proxy !== value
- 通常のオブジェクトとしての同一性で、Proxyインスタンスの同一性を評価する
p = new Proxy(t, h); p === p
new Proxy(t, h) !== new Proxy(t, h)
new Proxy(t, h) !== t
ここは、各Trapの引数や戻り値について、誤解しやすい点に注目したものです。
Trapの戻り値がbool (success or not)
である、いうのは、
false
を返すと、(strict modeのとき)Proxyアクセスした側で
TypeError
を発生させることを意味します。
const proxy = new Proxy({}, {
set(target, name, value, receiver) {
return false;
}
});
(function () {
"use strict";
proxy.foo = "foo"; //throw> TypeError
})();
Targetに対して実際に同種の処理を適用して成功したかどうかのことではありません。
const proxy = new Proxy({}, {
set(target, name, value, receiver) {
return true;
}
});
(function () {
"use strict";
proxy.foo = "foo"; //(pass silently)
console.log(proxy.foo); //=> undefined
})();
引数のname
はstring
かsymbol
です。
symbol
以外はすべてstring
化されます。
target
がArray
のインデックスアクセスであってもnumber
にはなりません。
const proxy = new Proxy([], {
get(target, name, receiver) {
return typeof name;
}
});
console.log(proxy[0]); //=> string
console.log(proxy[null]); //=> string
console.log(proxy[undefined]); //=> string
console.log(proxy[{}]); //=> string
console.log(proxy[new String("key")]); //=> string
console.log(proxy[Symbol("key")]); //=> symbol
get
とset
の最後の引数のreceiver
は、プロパティアクセスreceiver.property
の右のオブジェクトです。別の表現では、メソッド実行receiver.property()
で、
this
となるものです。普通はProxyになっているでしょう。
そうでない場合はたとえば、Proxyをプロトタイプにしたオブジェクトの
プロパティアクセスで、receiver
はそのオブジェクトになります。
const target = {};
const proxy = new Proxy(target, {
get(target, name, receiver) {
return receiver;
}
});
console.log(proxy.foo === proxy); //=> true
console.log(proxy.foo !== target); //=> true
// proxy as a prototype
const obj = Object.create(proxy);
console.log(Reflect.getPrototypeOf(obj) === proxy); //=> true
console.log(obj.foo === obj); //=> true
console.log(obj.foo !== proxy); //=> true
receiver
が重要なのは、プロパティがgetter/setterである時です。
receiver
は、getter/setter関数の中で使うthis
となるものです。
const target = {value: 5};
Object.defineProperty(target, "prop", {
get() {
return this.value * 10;
}
});
// use proxy value
const proxy = new Proxy(target, {
get(target, name, receiver) {
if (name === "value") return 2;
return Reflect.get(target, name, receiver);
}
});
console.log(proxy.prop); //=> 20;
// use target value
const buggy = new Proxy(target, {
get(target, name) {
if (name === "value") return 2;
return Reflect.get(target, name);
}
});
console.log(buggy.prop); //=> 50;
construct
TrapのnewTarget
は、
生成されたインスタンスのconstructor
プロパティになるコンストラクタ関数です。
通常のnew
呼び出しでは(getter/setterのreceiver同様に)proxy
となります。
const target = function Ctor() {};
const proxy = new Proxy(target, {
construct(target, args, newTarget) {
return {newTarget};
}
});
console.log(new proxy().newTarget === proxy); //=> true
console.log(new proxy().newTarget !== target); //=> true
また、Reflect.construct(ctor, args, newTarget)
を直接使うことで、自由に、
第三引数で(new
が可能な)function
オブジェクトをセットすることもできます。
この場合ではctor
が実行されたとき、その中でのthis.constructor
がnewTarget
に
なります(さらにnew.target
式の値もnewTarget
になります)。
単にconstructor
プロパティにセットされるだけであって、
newTarget
関数としては一切実行されません。
const ctor = function Ctor() {
this.newTarget = new.target;
};
console.log(Reflect.construct(ctor, []).newTarget === ctor); //=> true
const other = function Other() {
this.extra = true;
};
other.prototype.method = function () {
return "OK";
};
const obj = Reflect.construct(ctor, [], other);
console.log(obj.newTarget === other); //=> true
console.log(!(obj instanceof ctor)); //=> true
console.log(obj instanceof other); //=> true
console.log(obj.extra); //=> undefined
console.log(obj.method()); //=> "OK"
handler.isExtensible(target)
: boolean (extensible or not)handler.preventExtensions(target)
: boolean (prevent or not)
isExtensible
Trapは必ずReflect.isExtensible(target)
と同じ値を
返さなくてはいけません。結果の差し替えが不可能です。
const proxy = new Proxy({}, {
isExtensible(target) {return false;}
});
Object.isExtensible(proxy); //throw> TypeError
preventExtensions
Trapの戻り値は、false
だと「制約させられない」、
という意味になります。具体的にはObject.preventEextensions(proxy)
等
を呼ぶとTypeError
が出るようになります。
const proxy = new Proxy({}, {
preventExtensions(target) {
return false;
}
});
const o = Object.freeze(proxy); //throw> TypeError
preventExtensions
Trapの戻り値を別の値にすることは、
isExtensible(target)
がtrue
のときに、target
にタッチせずfalse
を返して
Object.preventExtensions
等をできないようにする場合などです。
handler.getPrototypeOf(target)
: prototype object (or undefined)handler.setPrototypeOf(target, proto)
: boolean (success or not)
ブラウザ拡張実装由来で、ES6 Annex Bで追認された
インスタンスごとのプロトタイプ設定obj.__proto__
の読み書きに相当する機能です。
obj.__proto__
の読み書きの代わりに、Reflect.getPrototypeOf(obj)
と
Reflect.setPrototypeOf(obj, proto)
が使えます。
ただしobject.__proto__
の実装はgetter/setterであり、その中で
getPrototypeOf(this)
/setPrototypeOf(this, proto)
が呼び出される仕様です。
Proxyオブジェクトへのproxy.__proto__
アクセスは、
まず__proto__
への
get(target, name, receiver)
/set(target, name, value, receiver)
Trapが
呼ばれ、デフォルトではそこでtarget
のgetter/setterが取り出され、
receiver
であるProxyをthis
にして実行されます。
そこからgetPrototyoeOf(proxy)
/setPrototyoeOf(proxy, proto)
が呼ばれ、
それによって
handler.getPrototypeOf(target)
/setPrototypeOf(target, proto)
Trapが
呼び出される、という仕組みになります。
const proxy = new Proxy({}, {
getPrototypeOf(target) {
return new Date();
},
get(target, name, receiver) {
return Reflect.get(target, name, receiver);
}
});
console.log(proxy.__proto__ instanceof Date); //=> true
get
/set
Trapで最後の引数receiver
を省略してしまうと、
receiver
がtarget
になり、
getPrototypeOf(target)
/setPrototypeOf(target, proto)
に直接
つながってしまい、
getPrototypeOf
/setPrototypeOf
Trapに届かなくなるので注意です。
const proxy = new Proxy({}, {
getPrototypeOf(target) {
return new Date();
},
get(target, name) {
return Reflect.get(target, name);
}
});
console.log(proxy.__proto__ instanceof Date); //=> false
handler.getOwnPropertyDescriptor(target, name)
: descriptor object (or undefined)handler.defineProperty(target, name, desc)
: boolean (success or not)
ES5でのObject.getOwnPropertyDescriptor(obj, name)
や
Object.defineProperty(obj, name, desc)
に対応して呼び出されるTrapです。
Object.defineProperty
の戻り値は設定したオブジェクト
またはTypeError
発生ですが、
Reflect.defineProperty
の戻り値はboolean
です。
あと注意点としては、configurable
がfalse
のときにReflect.defineProperty
実行が必ずfalse
になるわけではなく、value
も含めて同じ値になる
デスクリプタであれば結果はtrue
になります
(Object.defineProperty
の成否と同じ)。
const obj = {};
Reflect.defineProperty(obj, "prop", {configurable: false, value: 100});
console.log(
Reflect.defineProperty(obj, "prop", {configurable: false, value: 100})
); //=> true
console.log(
Reflect.defineProperty(obj, "prop", {configurable: false, value: 10})
); //=> false
Object.getOwnPropertyDescriptor
は数値や文字列に対しても正常として実行
しundefined
を返しますが、
Reflect.getOwnPropertyDescriptor
はインスタンス以外に
対して行うとTypeError
が発生します。
handler.has(target, name)
: booleanhandler.get(target, name, receiver)
: valuehandler.set(target, name, value, receiver)
: boolean (success or not)handler.deleteProperty(target, name)
: boolean (success or not)handler.ownKeys(target)
:Array
of names
has
Trapは、in
演算子や、with
にProxyをセットした時に
ブロック内での(let/const
以外の)変数へのアクセス等で発動します。
const proxy = new Proxy({}, {
has(target, name) {
if (name === "console") return false;
console.log("[has]", name);
return true;
}
});
"foo" in proxy; //=> "[has] foo"
with (proxy) {
var bar = 100; //=> "[has] bar"
}
get
/set
Trapでreceiver
が存在するのは、プロパティがgetter/setter関数
な場合、その関数で使うthis
となるオブジェクトを受け取るためです。
Reflect.get
やReflect.set
でtarget
にフォワードする場合では、
receiver
がproxy
になることで、target
のgetter/setter関数でのthis
への
プロパティアクセスから、さらにget
/set
Trapが発動されることになります。
ownKeys
Trapのデフォルト実装であるReflect.ownKeys(obj)
は、
以下の順でソートされたリストを返します:
- 非負整数値の文字列: 数値として昇順
- その他文字列: 追加順(chronological order)
- シンボル: 追加順
ただし、ownKeys
Trapの戻り値としては、
この順で整列されていなくてもTypeError
にならず、
Object.getOwnPropertyNames()
等ではその順のままで処理されます。
またこの内容をObject.keys(proxy)
やfor-in
ループで使用させる場合には、
返す名前に対応するプロパティデスクリプタでenumetable
をtrue
にして
返す実装も行う必要があります(さらにTargetに存在しないプロパティの場合は
configurable
もtrue
にする必要があります)。
const proxy = new Proxy({}, {
ownKeys(target) {return ["a", "bb", "ccc"];},
getOwnPropertyDescriptor(target, name) {
if (name.length % 2 === 1) return {
enumerable: true, configurable: true,
};
return undefined;
},
});
console.log(Object.keys(proxy)); //=> ["a", "ccc"]
isExtensible
やdesc.configurable
がfalse
の場合、
追加や変更だけでなく、
Targetにあるプロパティの存在を隠蔽することも
許されない点にも注意が必要でしょう。
たとえば、enumerable
なプロパティをownKeys
Trapの結果から省くこと
が許されません。
handler.apply(target, thisArg, args)
: valuehandler.construct(target, args, newTarget)
: object
まず、これらのTrapを使う場合、Targetがfunctionオブジェクトである
必要があります(typeof target === "function"
であること)。
const fproxy = new Proxy(() => {}, {
apply(target, thisArg, args) {
return "OK";
}
});
console.log(proxy()); //=> OK
const proxy = new Proxy({}, {
apply(target, thisArg, args) {
return "OK";
}
});
console.log(proxy()); //throw> TypeError
さらに、construct
Trapでは、Targetが、new
での使用が禁止される
Arrow Function(() => {}
等)および
メソッド({method() {}}.method
等)の場合は使えません(TypeError
)。
const fproxy = new Proxy(function () {}, {
construct(target, args, newTarget) {
return {OK: true};
}
});
console.log(new fproxy());
const aproxy = new Proxy(() => {}, {
construct(target, args, newTarget) {
return {OK: true};
}
});
try {
console.log(new aproxy());
} catch (er) {
console.log(er.message); //=> TypeError
}
const o = {
method() {}
};
const mproxy = new Proxy(o.method, {
construct(target, args, newTarget) {
return {OK: true};
}
});
try {
console.log(new mproxy());
} catch (er) {
console.log(er.message); //=> TypeError
}
construct
Trapでは、値でないオブジェクトを返す必要があります。
つまり、null
やnumber
やstring
は返せません。
しかし、返すオブジェクトは、コンストラクタが同じである必要も、
新しいインスタンスである必要もありません。
new
しても同じオブジェクトを返し続けるSingletonの実装も可能です。
const {proxy, revoke} = Proxy.revocable(target, handler);
でrevoke(無効化)可能なProxyを作ることができます。
このrevoke()
は関数で、呼び出した瞬間にproxy
はhandler
との関係性を
断ち切り、(Trapが作用しないtypeof
と===/!==
を除いて)
proxy
へのアクセスに対してTypeError
が発するようになります。
Revocableは、Object Capabilityでの代表的なパターンの一つです。 proxyを渡した相手に何か問題があったらrevokeしてそれ以降アクセス拒否できます。
逆のCapabilityとして、実績がたまったら制限がアンロックされていくようなProxy というのもできるでしょう。
-
ECMA262 ECMAScript Language Specification: Trap実装の失敗から
TypeError
が出るのは、"Essential Internal Method"の"Invariant"や "Abstract Operation"の各操作のチェックに 違反するためです。操作の中で入れ子で操作が使われることもあるので、 直接Proxyに言及してある以外の部分も解釈する必要があります。 -
MDN Proxy handler: Trap単位で上記仕様から導いた不変条件をまとめてあります。 古い仕様も混ざっている点、及び、日本語訳は単語が過剰に訳されている ("非設定" => configurableがfalse、など)点には注意。
-
Object Capability Model: Capabilityは独立した複数の主体のあいだでプログラムと オブジェクトを相互に動かしあうことが前提にあります。 オブジェクトの受け手が静的に(interfaceの制限によって)関係ないものは扱わない ようにするのとは、逆に、 オブジェクトの送り手が動的に必要な分だけアクセスできるようにしたハンドル (facet, forwarder, proxy)を作って渡す、という考え方をするようです
-
You Don't Know JS (book series): ECMAScript自体の言語構造について 理解するための本です。 言語自体を扱う話では、このレベルの知識は前提として必要でしょう。 Proxyについては"ES6 & Beyond"の7章に少しあります。