Proxyについて
概要
Proxyを使うとオブジェクトに対する様々な操作に割り込み、好きな振る舞いをさせることが出来る。
記事更新履歴
[2016/03/23] ES2016でenumerateトラップが削除されるのに対応
[2015/12/02] 公開
APIの概要
提供されるメソッド
new Proxy( target<Object>, handler<Object> ) -> <Proxy> // targetオブジェクトを基盤としたプロキシを作る // プロキシへの操作を受ける関数を入れたhandlerオブジェクトを指定する Proxy.revocable ( target<Object>, handler<Object> ) -> { proxy<Proxy>, revoke<Function> } // 無効化可能なプロキシをproxyプロパティに持ち、 // 無効化するためのrevokeメソッドを持ったオブジェクトが返される
基本的な使い方
第一引数に基盤にしたいtargetオブジェクト、第二引数にプロキシへの操作を受けるトラップ関数を含んだhendlerオブジェクト指定する。
プロキシに対してプロパティへのアクセスや列挙などが行われるとき、
プロキシへの操作の種類に対応するトラップ関数が定義されていた場合はそれが呼ばれ、関数が返す値に従って処理が行われる。
定義されていなかった場合は、targetオブジェクトにリダイレクトされる。
let o = { a: 1, b: 2, c: 3 } // プロパティ取得に関して受け常に数値42を返すプロキシを作る let p = new Proxy( o, { get( target, key, receiver ) { return 42 } } ) p.a // 42 p.x // 42 // プロパティ取得以外の操作はスルーされる Object.keys(p) // ["a", "b", "c"]
無効化可能なプロキシを作る際には、Proxy.revocableを使う。
無効化関数が呼ばれた後にプロキシを使おうとするとエラーとなる。
let o = { a: 1 } let { proxy: p, revoke } = Proxy.revocable( o, { get( target, key, receiver ) { return 42 } } ) p.a // 42 revoke() p.a // TypeError
捕捉タイプ別説明
元の動作を再現する時はReflect APIが便利である。
has
プロパティの(プロトタイプまで含んだ)存在判定を捕捉する。
トラップにはtargetと、取得されることが期待されたキー(文字列またはシンボル)であるkeyが渡される。
トラップが返す真偽値が判定の結果とみなされる。
let o = { } let p = new Proxy( o, { has( target, key ) { console.log( target, key ) return true } } ) 'a' in o // false 'a' in p // true //*log* o, "a"
ただし、Proxyだからと言って完全に自由に振る舞えるわけではない。
ある程度の処理の意味や整合性を保つために、大きく現実と矛盾する結果を返すことは禁止される。
例えばトラップがfalsyな値を返した場合で、存在確認されるプロパティがtargetに存在した場合、
その属性が編集禁止であったり、targetが拡張禁止な場合はエラーとなる。
let o = Object.freeze( { a: 1 } ) let p = new Proxy( o, { has( ) { return false } } ) 'a' in p // TypeError
get
プロパティの取得を捕捉する。
トラップにはtarget、key、処理を期待した大本であるreceiverが渡される。
トラップが返す値が取得された値とみなされる。
let o = { } let p = new Proxy( o, { get( target, key, receiver ) { console.log( target, key, receiver ) return 42 } } ) // 例えばドット演算子の左辺値がreceiverとなる。 p.a // 42 //*log* o, "a", p let p2 = { __proto__: p } p2.a // 42 //*log* o, "a", p2
ただし取得されるプロパティに関して、targetでの属性が編集禁止である場合、
ゲッタ定義のないアクセサであったり、書込禁止の値でトラップが返す値と異なった場合はエラーとなる。
set
プロパティの設定を捕捉する。
トラップにはtarget、key、と設定されることが期待された値であるvalue、そしてreceiverが渡される。
トラップが返す真偽値が処理の成否とみなされる。
let o = { } let p = new Proxy( o, { set( target, key, value, receiver ) { console.log( target, key, value, receiver ) return Reflect.set( target, key, 42 ) } } ) p.a = 123 //*log* o, "a", 123, p o.a // 42
ただし、トラップがtruthyな値を返した場合で、設定されるプロパティに関して、targetでの属性が編集禁止である場合、
セッタ定義のないアクセサであったり、書込禁止の値でvalueと異なった場合はエラーとなる。
deleteProperty
プロパティの削除を捕捉する。
トラップにはtarget、keyが渡される。
トラップが返す真偽値が処理の成否とみなされる。
let o = { a: 1 } let p = new Proxy( o, { deleteProperty( target, key ) { return true } } ) delete a in p // true // なにも処理をしてないので実際は残っている o.a // 1
ただし、トラップがfalsyな値を返した場合で、targetでの属性が編集禁止である場合はエラーとなる。
getOwnPropertyDescriptor
固有プロパティディスクリプタの取得を捕捉する。
トラップにはtarget、keyが渡される。
トラップが返すオブジェクトがディスクリプタとみなされる。
let o = { } let p = new Proxy( o, { getOwnPropertyDescriptor( target, key ) { return { value: 123, configurable: true } } } ) Object.getOwnPropertyDescriptor( p, 'a' ) // { value: 123, writable: false, enumerable: false, configurable: true }
ただしtargetでの属性が編集禁止である場合、
その他の属性がトラップの返り値の属性と違う場合(書込可であればデータ値のみは異なっても良い)エラーとなる。
また編集可の場合も、トラップの返り値での属性が編集不可である場合はエラーとなる。
defineProperty
プロパティの定義を捕捉する。
トラップにはtarget、keyとプロパティの属性を指定するためのattributesオブジェクトが渡される。
トラップが返す真偽値が処理の成否とみなされる。
let o = { } let p = new Proxy( o, { defineProperty( target, key, attributes ) { console.log( target, key, attributes ) return Reflect.defineProperty( target, key, attributes ) } } ) Object.defineProperty( p, 'a', { value: 1 } ) //*log* o, "a", { value: 1 } // Reflect APIでリダイレクトしたので元オブジェクトに反映されている。 o.a // 1
ただし、トラップがtruthyな値を返した場合で、targetでの属性が編集禁止である場合、
その他の属性がattributesと違う場合(書込可であればデータ値のみは異なっても良い)エラーとなる。
また編集可の場合も、attributesでの属性が編集不可である場合はエラーとなる。
ownKeys
固有プロパティキー一覧の取得を捕捉する。
トラップにはtargetが渡される。
トラップが返す配列がキー一覧としてみなされる。
let o = { a: 1, b: 2, c: 3 } let p = new Proxy( o, { ownKeys( target ) { return [ 'a' ] } } ) Object.assign( { }, p ) // { a: 1 } // プロキシが固有キーを"a"しか申告しないので"a"プロパティしか合成されない
ただし、targetが拡張可な場合にtargetにて存在するプロパティキーのうち編集禁止のものを1つでも含めなかったり、
targetが拡張禁止な場合にtargetにて存在するプロパティキーと全く同じ一覧を返さない場合はエラーとなる。
enumerate(ES2016で廃止)
プロトタイプまで含む文字列のプロパティキーの列挙を捕捉する。
トラップにはtargetが渡される。
トラップが返すオブジェクトが列挙のためのイテレータとしてみなされる。
let o = { } let p = new Proxy( o, { *enumerate( target ) { yield* 'abc' } } ) for ( let k in p ) { console.log( k ) } //*log* "a" -> "b" -> "c"
※このトラップはES2016で廃止されました。
※for-in文ではownKeys,getOwnPropertyDescriptor,getPrototypeOfの3トラップが利用されます。
getPrototypeOf
プロパティの定義を捕捉する。
トラップにはtargetが渡される。
トラップが返すオブジェクトまたはnullがプロトタイプとみなされる。
let o = { } let p = new Proxy( o, { getPrototypeOf( target ) { return { hoge: 123 } } } ) p.__proto__ // { hoge: 123 }
ただし、targetが拡張禁止な場合、トラップの返り値とtargetのプロトタイプが異なる場合はエラーとなる。
setPrototypeOf
プロパティの定義を捕捉する。
トラップにはtargetと、設定されるプロトタイプオブジェクトまたはnullであるprotoが渡される。
トラップが返す真偽値が処理の成否とみなされる。
let o = { } let p = new Proxy( o, { setPrototypeOf( target, proto ) { console.log( target, proto ) return false // 常に失敗を返す } } ) p.__proto__ = { hoge: 123 } // TypeError //*log* o, { hoge: 123 } // Object.prototype.__proto__セッタは、プロトタイプ設定処理が失敗すると例外を吐く
ただし、トラップtruthyな値を返した場合で、targeが拡張禁止である場合、
トラップの返り値とtargetのプロトタイプが異なる場合はエラーとなる。
isExtensible
拡張可かどうかの判定を捕捉する。
トラップにはtargetが渡される。
トラップが返す真偽値が判定の結果とみなされる。
let p = new Proxy( o, { isExtensible( target ) { console.log( target ) return Reflect.isExtensible( target ) } } ) Object.isFrozen( p ) //*log* o
ただし、targetの拡張可否と異なる結果を返した場合はエラーとなる。
つまりこのトラップはロギングなど利用用途が限られる。
preventExtensions
拡張禁止設定を捕捉する。
トラップにはtargetが渡される。
トラップが返す真偽値が処理の成否とみなされる。
let o = { } let p = new Proxy( o, { preventExtensions( target ) { console.log( target ) return false // 常に失敗を返す } } ) Object.freeze( p ) // TypeError //*log* o // Object.freeze関数は、オブジェクト凍結処理が失敗すると例外を吐く
ただし、トラップがtruthyな値を返した場合で、targeが拡張禁止でない場合はエラーとなる。
apply
関数呼び出しを捕捉する。
トラップにはtargetと、呼び出し元であるthisArg、引数配列のargsが渡される。
トラップが返す値が返り値とみなされる。
let f = ( ) => { } let p = new Proxy( f, { apply( target, thisArg, args ) { console.log( target, thisArg, args ) return 42 } } ) p.apply( 0, [ 1, 2, 3 ] ) // 42 //*log* f, 0, [ 1, 2, 3 ]
ただしこのプロキシのtargetは関数でないといけない。
construct
コンストラクタ呼び出しを捕捉する。
トラップにはtarget、argsと、呼びだされ対象であるnewTargetが渡される。
トラップが返すオブジェクトが返り値とみなされる。
let c = class { } let p = new Proxy( c, { construct( target, args, newTarget ) { console.log( target, args, newTarget ) return { } } } ) new p( 1, 2, 3 ) //*log* c, [ 1, 2, 3 ], p
ただしこのプロキシのtargetはコンストラクタでないといけない。
応用例
拡張する
Proxyを使えば既存の物を自在に拡張したり、新しく組み立て直したりできる。
例1
他言語には、配列の添字に負数を指定して最後からの位置を指定できるものもある。
これを実装できないか考える。
利用イメージ:
let ary = new ExArray( 1, 2, 3 ) console.log( ary[ -1 ] ) // 3
ExArrayの実装:
class ExArray extends Array { constructor( ...args ) { return new Proxy( super( ...args ), { get( tgt, key, rec ) { if ( +key < 0 ) key = tgt.length + +key return Reflect.get( tgt, key, rec ) }, set( tgt, key, val, rec ) { if ( +key < 0 ) key = tgt.length + +key return Reflect.set( tgt, key, val, rec ) } } ) } }
こういったときは、配列の機能を殆どArrayに頼るためにsuper()したものをターゲットにする。
あとは、get,setにおいてkeyが負数文字列のとき、それをlengthに足した値に置き換えてやればよい。
例2
NodeListにElementと同じメソッドを生やせないか考えてみる。
NodeListのプロトタイプをただElement.prototypeに接続しても、
ElementのメソッドはElementを対象としているため当然使えない。
そこでElementのメソッドをNodeList用に変換するプロキシで包んで接続してやれば上手くいく。
利用イメージ:
<p>A</p> <p>B</p> <p>C</p>
let nl = document.querySelectorAll( 'p' ) console.log( nl.innerHTML ) // [ "A", "B", "C" ]
それなりな実装:
NodeList.prototype.__proto__ = new Proxy( Element.prototype, { get( tgt, key, rec ) { const desc = Reflect.getOwnPropertyDescriptor( tgt, key, rec ) const val = desc && ( desc.value || desc.get ) if ( typeof val != 'function' ) return const fn = ( ...args ) => Array.from( rec ).map( elm => Reflect.apply( val, elm, args ) ) return desc.get ? fn() : fn } } )
NodeListがプロパティをプロトタイプに探索に来たとき、
プロパティがElements.prototypeに関数値として存在していれば、NodeListの各Elementに適応する関数を返す。
またゲッタとして存在していれば、ゲッタを各要素に適応し、結果をまとめて配列として返している。
with文と組み合わせる
with文は、指定したオブジェクトをスコープに組み込む事ができる構文である。
つまりプロキシを指定すると、そのプロキシがwith文内の変数アクセスを自在に制御することができる。
例1
with文中に隔離されたスコープを提供するIsolatedScopeコンストラクタを考える。
但し、完全に隔離してしまうと外部とやり取りする方法がないので、指定したオブジェクトをスコープに追加する機能と、
ついでに変数オブジェクトにアクセスできる変数を提供する機能もつける。
利用イメージ:
with( new IsolatedScope( { console }, '$' ) ) { a = 42 $.b = $.a console.log( b ) // 42 }
IsolatedScopeコンストラクタのそれなりな実装:
function IsolatedScope( imports, exposeVar = '' ) { const variableMap = new Map // 変数を入れておくマップを作る const bindingProxy = new Proxy( { __proto__: null }, { has( _, key ) { return true }, get( _, key ) { // マップに無ければimportsから取ってくる return variableMap.has( key ) ? variableMap.get( key ) : Reflect.get( imports, key ) }, set( _, key, val ) { // exposeVar変数を上書きさせない if ( key == exposeVar ) return false variableMap.set( key, val ) return true }, deleteProperty( _, key ) { // exposeVar変数を消させない if ( key == exposeVar ) return false variableMap.delete( key ) return true } } ) variableMap.set( exposeVar, bindingProxy ) // exposeVar変数としてプロキシを提供する return bindingProxy }
まず、with文中で変数を得る必要ができたとき、最初に「has」トラップが呼ばれる。
ここでfalseを返せばプロキシはその変数を把握していないと見られ、アクセスがwith文を抜ける。
今回はそれでは困るので、全てのキーにおいてtrueを返し、その後の処理をプロキシに対して行わせている。
その後変数アクセスの種類に応じてトラップが呼ばれる。
例2
with-proxyを使って、bignum(多倍長整数)の演算ゾーンを実現できないか考える。
bignum型を新しく作る定義するのは文字列型を使うとして、問題となるのは演算である。
「c = a + b」の時に、cにaとbの和を代入する処理だとプロキシは知ることができるだろうか?
結論から言うとできる。
変数参照時にそれぞれ変数固有の特殊な数値を返せばいいのである。
すると、プロキシはcにsetされる数値から演算を逆算でき、bignumとしての演算をシミュレートすることができる。
(逆に言うとどのような演算が行われても逆算できそうな数値を変数に紐付けておいて返す)
最後に実際のbignum文字列を変数から取り出す手段を用意すれば完成である。
利用イメージ:
let a = '767765657458686565862575867948' with( new BigNumScope( { console, a } ) ) { b = '124014192430129650919047594163' c = a + b console.log( $( c ) ) // "891779849888816216781623462111" }
BigNumScopeのそれなりな実装(正の数、一度の代入文で加算演算1つだけに対応):
function BigNumScope( imports = { __proto__: null } ) { const varMap = new Map // key -> {numStr, magic} const magicMap = new Map // magic -> key const magicList = [ ] let varCount = 0 function getOrGgenMagic( key ) { if ( varMap.has( key ) ) return varMap.get( key ).magic const magic = magicList[ magicList.length ] = ( magicList[ magicList.length - 1 ] | 0 ) + 1.01 magicMap.set( magic, key ) return magic } function chkNumSrt( str ) { if ( !/^\d+$/.test( str ) ) throw 'Illegal numStr' return str } function solveMagic( val ) { if ( magicMap.has( val ) ) return varMap.get( magicMap.get( val ) ).numStr for ( let m1 of magicList ) { for ( let m2 of magicList ) { if ( m1 + m2 == val ) return calcMagic( 'add', m1, m2 ) if ( m1 - m2 == val ) return calcMagic( 'sub', m1, m2 ) if ( m1 * m2 == val ) return calcMagic( 'mul', m1, m2 ) if ( m1 / m2 == val ) return calcMagic( 'div', m1, m2 ) } } throw 'Cannot solve magic.' } function calcMagic( type, m1, m2 ) { const s1 = varMap.get( magicMap.get( m1 ) ).numStr const s2 = varMap.get( magicMap.get( m2 ) ).numStr let s3 = '' const maxSize = Math.max ( s1.length, s2.length ) switch ( type ) { case 'add': for ( let i = 0, mp = 0; i < maxSize / 15; i++ ) { const n1 = +s1.slice( -15 * ( i + 1 ), -15 * i || undefined ) const n2 = +s2.slice( -15 * ( i + 1 ), -15 * i || undefined ) const n3 = n1 + n2 + mp mp = n3 / 1e15 | 0 s3 = ( '0'.repeat(15) + n3 % 1e15 ).slice( -15 ) + s3 } s3 = s3.replace( /^0+/, '' ) || '0' break; default: throw `${ type } operation is not supported.` } return s3 } return new Proxy( { __proto__: null }, { has( ) { return true }, get( _, key ) { // keyが$ならmagicをとってbignum文字列を返す関数を返す if ( key == '$' ) return magic => varMap.get( magicMap.get( magic ) ).numStr if ( Reflect.has( imports, key ) ) { const v = Reflect.get( imports, key ) // importsにあるオブジェクトなら if ( typeof v == 'function' || typeof v == 'object' ) return v // そのまま返し、非オブジェクトなら varMap.set( key, { numStr: '' + v, magic: getOrGgenMagic( key ) } ) // bignumとして変数マップ取り込む } if ( varMap.has( key ) ) return varMap.get( key ).magic // 変数マップにヒットすればその特殊数値を返す if ( typeof key == 'symbol' ) return undefined throw `${ String( key ) } is not defined` }, set( _, key, val ) { let newNumStr switch ( typeof val ) { case 'string': newNumStr = chkNumSrt( val ) // 文字列はそのままbignumとして解釈し break; case 'number': newNumStr = solveMagic( val ) // 数値はmagicNum同士の演算結果と解釈して逆算する break; default: throw 'Illegal set variable' } varMap.set( key, { numStr: newNumStr, magic: getOrGgenMagic( key ) } )// keyとbignum文字列と特殊数値を紐付ける return true }, } ) }
これを応用すればあらゆる数値型は勿論、様々な振る舞いをする型を作ることが出来る。
その他応用例
追記予定
実装されるバージョン
V8 4.9.185(概ね標準通りの挙動) 4.9.354(デフォルト有効)