【JavaScript】 Proxyオブジェクトの使い方と仕組み
更新日:2024/04/26
JavaScriptには標準でProxyオブジェクトが定義されています。
「これって何だ?」
ということで、Proxyオブジェクトの使い方について調べたので解説してみます。
また、普通とは異なる動きをするProxyオブジェクトの仕組みについてもお伝えします。
- 1Proxyの概要
- Proxyのインスタンスの作成
- デフォルトの動作
- トラップ(代理処理)の概要
- Proxyによる処理速度の低下
- 2Proxyのハンドラー関数
- applyハンドラー
- constructハンドラー
- definePropertyハンドラー
- deletePropertyハンドラー
- getハンドラー
- getOwnPropertyDescriptorハンドラー
- getPrototypeOfハンドラー
- hasハンドラー
- isExtensibleハンドラー
- ownKeysハンドラー
- preventExtensionsハンドラー
- setハンドラー
- setPrototypeOfハンドラー
- 3Proxyの仕組み
- Object.isExtensible()の処理
- Proxyインスタンスの構造
- proxy.[[IsExtensible]]の処理
- ハンドラーと内部メソッドの関係
Proxyの概要
Proxyを日本語に訳すると代理という意味になります。
JavaScriptのProxyオブジェクトは元となるオブジェクトの一部の処理を代行します。
Proxyオブジェクトが対象とするオブジェクトは、ターゲットオブジェクトまたは単にターゲットと呼ばれます。
Proxyのインスタンスの作成
Proxyオブジェクトはコンストラクターなので、new演算子でインスタンス作成して使用します。
Proxyのインスタンス作成は、次の構文でおこないます。
Proxyの構文
new Proxy( ターゲットオブジェクト , ハンドルオブジェクト)
ターゲットオブジェクトは、元となるオブジェクトです。
ハンドルオブジェクトは、特定の処理時に呼び出されるコールバック関数を定義したものです。
デフォルトの動作
デフォルトの動作を確認するため、空のハンドルオブジェクトでインスタンスを作成してみます。
const obj = {};
const proxyObj = new Proxy( obj , {});
console.log( proxyObj ); // Proxy { <target>: {…}, <handler>: {} }
// proxyObjを変更
proxyObj.prop = 100;
proxyObj.func = ()=>{};
// proxyObjは変化なし
console.log( proxyObj ); // Proxy { <target>: {…}, <handler>: {} }
// objが変化している!
console.log( obj ); // Object { prop: 100, func: func() }
上のコードは、ターゲットオブジェクトobjからProxyインスタンスproxyObjを作成しています。
そして作成したproxyObjに対して、プロパティの作成と値のセットをおこなっています。
結果を見ると、proxyObjにプロパティが追加されていません。
実際にプロパティが作成されたのはobjです。
proxyObjはobjの代わりに処理を受け付け、その後の処理をobjに丸投げしているのです。
トラップ(代理処理)の概要
ハンドルオブジェクトに値のセットをトラップするコールバック関数を指定し、Proxyインスタンスを生成してみます。
const obj = {};
const proxyObj = new Proxy( obj ,{
set:function ( targetObj , propName , value ){} // ←ハンドラー関数set
}
proxyObj.prop = 100; // ①
proxyObj.func = ()=>{}; // ②
console.log( proxyObj ); // Proxy { <target>: {…}, <handler>: {} }
console.log( obj ); // Object { } ←変化なし
ハンドルオブジェクトのプロパティはハンドラー関数と呼ばれ、既定の名前を持っています。
上のコードのハンドラー関数setは、割り当て演算子(=)などでプロパティに値がセットされるときに呼び出される関数です。
このコードは①と②でProxyのプロパティに値をセットしています。
この操作をおこなったことで、前回はターゲットオブジェクトに値がセットされました。
しかし、今回はセットされていません。
ハンドラー関数で処理を受け付けた場合、既定の動作がおこなわれないからです。
そのため、ハンドラー関数内で既定の動作に関する処理をおこなう必要があります。
実際にやってみます。
上のコードに、objに値をセットする処理を追加してみます。
const obj = {};
const proxyObj = new Proxy( obj ,{
set:function ( targetObj , propName , value ){
targetObj[ propName ] = value;
}
}
proxyObj.prop = 100;
proxyObj.func = ()=>{};
console.log( proxyObj ); // Proxy { <target>: {…}, <handler>: {} }
console.log( obj ); // Object { prop: 100, func: func() } ←変化あり
ハンドラー関数setの第一引数targetObjは、インスタンス作成時の第一引数です。
つまりobjです。
第二第三引数はそれぞれプロパティ名と値です。それらを使用してobjにプロパティをセットしています。
その結果、objにプロパティが作成され値がセットされました。
Proxyによる処理速度の低下
Proxyのハンドラー関数にトラップされると、トラップされないときと比較して処理時間が長くなります。
内部的な処理では、ハンドラー関数を実行した後様々な検証がおこなわれます。
この検証はトラップされないときは実施されません。
そのため、速度差が出ます。
詳しくは、この記事のProxyの仕組みをご覧ください。
Proxyのハンドラー関数
Proxyオブジェクトは次の表のハンドラーを定義できます。
ハンドラー名 | ハンドラーを呼び出す機能・メソッド |
---|---|
apply | 関数( ) |
construct | new 関数( ) |
defineProperty | Object.defineProperty( ) |
deleteProperty | delete オブジェクト.プロパティ |
get | オブジェクト.プロパティ または オブジェクト[ プロパティ ] |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor( ) |
Reflect.getPrototypeOf | Object.getPrototypeOf( ) |
has | プロパティ in オブジェクト |
isExtensible | Object.isExtensible( ) |
ownKeys | Object.getOwnPropertyNames( ) , Object.getOwnPropertySymbols( ) |
preventExtensions | Object.preventExtensions( ) |
set | オブジェクト.プロパティ = 値 または オブジェクト[ プロパティ ] = 値 |
setPrototypeOf | Object.setPrototypeOf( ) |
ハンドラー名と同名のReflectオブジェクトのメソッドを呼び出すと、トラップされます。
const proxy = new Proxy( {} , { get:()=>{} , set:()=>{} } );
Reflect.get(proxy , "prop" ); // get:()=>{} でトラップされる
Reflect.set(proxy , "prop" , 100 ); // set:()=>{} でトラップされる
Reflectオブジェクトについては、次のページを読んでみてください。
■【JavaScript】 組み込みオブジェクトReflectって何?
applyハンドラー
applyハンドラーは、関数( )や関数.call()、関数.apply()、Reflect.apply()などで関数を実行するとき呼び出されます。
構文
apply: function( ターゲットオブジェクト , this値 , 引数配列 )
実行例:
const func = function (a,b,c){ return a * b * c ; }
const proxy = new Proxy( func , {
apply:function(target, thisArg, argArray) {
console.log( `結果: ${ //____TAGFOLDER____//( ...arguments ) }` );
}
});
proxy( 1, 2, 3);
補足1:
applyハンドラーが、関数オブジェクト以外を返すとエラーになります。
補足2:
applyハンドラーは関数が実行されるときに呼び出されます。
JavaScriptのシステムは実行前に関数オブジェクトかどうかチェックします。
そのためProxyに関数以外のオブジェクトを指定しても呼び出されません。
その前にエラーになります。
関数オブジェクト以外は実行できない!
const proxy = new Proxy( func , {
apply:function(target, thisArg, argArray) {}
});
proxy( ); // TypeError: proxy is not a function
ターゲットオブジェクト内のメソッドにapplyハンドラーを適用したいときは、ターゲットオブジェクトのgetハンドラーでメソッドのProxyを返すなどの工夫が必要です。
メソッドにapplyハンドラーを適用
const obj = {
func : function (a,b,c){ return a * b * c ; }
};
const funcProxy = new Proxy( obj.func , {
apply:function(target, thisArg, argArray) {
console.log( `結果: ${ //____TAGFOLDER____//( ...arguments ) }` );
}
});
const proxy = new Proxy( obj , {
get : function(target, prop, receiver) {
if( prop === "func" ) return funcProxy;
return target[prop]
}
});
proxy.func( 1, 2, 3);
constructハンドラー
constructハンドラーは、new演算子やReflect.construct()を実行するとき呼び出されます。
このハンドラーは、実行結果としてオブジェクトを返す必要があります。
構文
construct: function( ターゲットオブジェクト , 引数配列 , newオブジェクト )
newオブジェクトはnew演算子を実行したオブジェクトで、通常はproxyです。
実行例:
const func = function(a,b,c){
[this.a,this.b,this.c] = arguments;
};
const proxy = new Proxy( func , {
construct:function( target, argumentsList, newTarget ){
const result = Reflect.construct( ...arguments )
result.summary = function(){ return this.a + this.b + this.c; };
return result;
}
});
console.log( (new proxy( 1 , 2 , 3)).summary() ); // 6
補足1:
constructハンドラーが、オブジェクト以外を返すとエラーになります。
補足2:
constructハンドラーは、applyハンドラーと同じように関数オブジェクトが対象となります。
definePropertyハンドラー
definePropertyハンドラーはObject.defineProperty() またはReflect.defineProperty()の対象としてオブジェクトが指定されたとき、呼び出されます。
このハンドラーは、真偽値を返す必要があります。
構文
defineProperty: function( ターゲットオブジェクト , プロパティ名 , プロパティ記述子 )
実行例:
const obj = {};
const proxy = new Proxy( obj , {
defineProperty:function( target, property, descriptor ){
// propから始まるプロパティのみ受け付け
if( !property.startsWith( "prop" ) ) return false;
return Reflect.defineProperty( ...arguments );
}
});
try{
Object.defineProperty( proxy , "prop1" , {value:100} );
Object.defineProperty( proxy , "data1" , {value:200} );
}catch (e) {
console.log( e ); // TypeError: proxy defineProperty handler returned false for property '"data1"'
}
console.log( obj ); // Object { prop1: 100 }
definePropertyハンドラー実行後の検証:
definePropertyハンドラーでfalseをリターンすると、エラーがスローされます。
trueをリターンすると、内部で次のような検証がおこなわれます。
■definePropertyハンドラー実行後の検証内容
※ここではターゲットオブジェクトをobjとし、指定したプロパティ記述子を指定記述子としています。
▶objにプロパティが無い場合(ハンドラーで何も処理せずにtrueを返したときなど)
- Object.freeze()などでobjが変更不可になっているなら、エラーがスローされます。
- 指定記述子にconfigurable属性があり、値がfalseのときエラーがスローされます。configurableの規定値はfalseですが、指定記述子に設定がなければエラーになりません
▶objにプロパティがある場合
変更後のobjからプロパティ記述子が取得されます。ここでは変更後記述子とします
- 指定記述子がconfigurable属性を持っていて、変更後記述子のconfigurable属性と異なる場合、エラーがスローされます。
以下は変更後記述子のconfigurable属性がfalseのときに適用されます
- 指定記述子がenumerable属性を持っていて、変更後記述子の同属性と異なる場合、エラーがスローされます。
- 指定記述子と変更後記述子のタイプ(データまたはアクセサー)が異なるなら、エラーがスローされます。
以下は変更後記述子のconfigurable属性がfalse、かつ、変更後記述子がアクセサー記述子のときに適用されます
- 指定記述子がget属性を持っていて、変更後記述子の同属性と異なる場合、エラーがスローされます。
- 指定記述子がset属性を持っていて、変更後記述子の同属性と異なる場合、エラーがスローされます。
以下は変更後記述子のconfigurable属性がfalse、かつ、変更後記述子がデータ記述子のときに適用されます
- 指定記述子がwritable属性を持っていて、変更後記述子の同属性と異なる場合、エラーがスローされます。
- 変更後記述子のwritable属性がfalseで、指定記述子がvalue属性を持っていて、変更後記述子のvalue属性と異なる場合、エラーがスローされます。
とりあえず、テクニカルなことをやろうとするとエラーになる可能性が高いと思っておくといいですね。
deletePropertyハンドラー
deletePropertyハンドラーは、delete演算子を実行するとき呼び出されます。
このハンドラーは、実行結果として真偽値を返す必要があります。
構文
deleteProperty: function( ターゲットオブジェクト , プロパティ名 )
実行例:
const proxy = new Proxy( { } , {
deleteProperty:function( target, property ){
if( !target.hasOwnProperty( property ) )
throw new ReferenceError( `存在しないプロパティ"${property}"を削除しようとしました` );
return Reflect.deleteProperty( ...arguments );
}
});
try{
delete proxy.p;
}catch (e) {
console.log( e ); // ReferenceError: 存在しないプロパティ"p"を削除しようとしました
}
補足:
プロパティのconfigurable属性がfalseまたはターゲットオブジェクトが変更不可のとき、ハンドラーでtrueを返すとエラーになります。
getハンドラー
getハンドラーは、プロパティが存在するしないにかかわらず、プロパティから値が取得されるとき呼び出されます。
構文
get: function( ターゲットオブジェクト , プロパティ名 , proxyオブジェクト )
実行例:
const proxy = new Proxy( { prop:100 } , {
get:function( target, property ){
if( !(property in target) )
throw new ReferenceError( `存在しないプロパティ"${property}"から値を取得しようとしました` );
return target[ property ];
}
});
try{
console.log( proxy.prop ); // 100
console.log( proxy.data );
}catch (e) {
console.log( e ); // ReferenceError: 存在しないプロパティ"data"から値を取得しようとしました
}
補足1:
プロパティが変更不可のとき、実際の値と異なるものを返すとエラーになります。
const obj = Object.defineProperty( { } , "prop" , {
value:200
});
const proxy = new Proxy( obj, {
get:function ( target, property ){
return 100;
}
});
const value = proxy.prop ;
// TypeError: proxy must report the same value for the non-writable, non-configurable property '"prop"'
補足2:
configurable属性がfalseでゲッターが定義されていないアクセサープロパティが、undefined以外を返すとエラーになります。
const obj = Object.defineProperty( { } , "prop" , {
set:function( value ){ }
});
const proxy = new Proxy( obj , {
get:function ( target, property ){
return 100;
}
} );
console.log( proxy.prop );
// TypeError: proxy must report undefined for a non-configurable accessor property '"prop"' without a getter
補足3:
ターゲットオブジェクトにセッタープロパティが存在していてその関数内でthisを参照しているとき、Proxyをthisとして扱いたいケースがあります。
次のようなオブジェクトを例にして考えてみます。
const obj = Object.defineProperty({data1:100, data2:200},"sum",{
get:function(){ return this.data1 + this.data2 }
});
console.log( obj.sum ); // 300
このオブジェクトからProxyを生成します。
このときgetハンドラーで値を10倍して返します。
const proxy = new Proxy( obj , {
get:function( target, property ){
if( property === "sum" ) return target[ property ];
return target[ property ] * 10;
}
});
console.log( proxy.data1 ); // 1000
console.log( proxy.data2 ); // 2000
console.log( proxy.sum ); // 300 ← obj.data1 + obj.data2の結果
data1とdata2は、共に10倍の値を取得できました。
しかしsumはターゲットオブジェクトの値を合計しているので、変化なしです。
proxy経由で値を取得しているので、できればproxy.data1とproxy.data2を合計した値を得たいです。
こんなときは、Reflect.get()を使用します。
Reflect.get()の3番目の引数は、ゲッター関数内でthis値として使用されます。
const proxy = new Proxy( obj , {
get:function( target, property , receiver){
if( property === "sum" ) return Reflect.get( ...arguments );
return target[ property ] * 10;
}
});
console.log( proxy.sum ); // 3000 ← proxy.data1 + proxy.data2の結果
getOwnPropertyDescriptorハンドラー
getOwnPropertyDescriptorハンドラーは、Object.getOwnPropertyDescriptor()またはReflect.getOwnPropertyDescriptor()の対象となったとき、呼び出されます。
このハンドラーは、オブジェクトまたはundefinedを返す必要があります。
構文
getOwnPropertyDescriptor: function( ターゲットオブジェクト , プロパティ名 )
実行例:
const proxy = new Proxy( { data1:100 } , {
getOwnPropertyDescriptor:function( target, property ){
if( !target.hasOwnProperty( property ) )
throw new ReferenceError( `存在しないプロパティ"${property}"を参照しました` );
return Reflect.getOwnPropertyDescriptor( ...arguments );
}
});
try{
console.log( Object.getOwnPropertyDescriptor(proxy,"data1") );
console.log( Object.getOwnPropertyDescriptor(proxy,"data2") );
}catch (e) {
console.log( e ); // ReferenceError: 存在しないプロパティ"data2"を参照しました
}
補足:
ハンドラー関数実行後、ハンドラー関数で返された値に対してdefinePropertyハンドラーに似た検証が行われます。
そのため実際の値と異なる記述子を返した場合、エラーになる可能性が高いです。
getPrototypeOfハンドラー
getPrototypeOfハンドラーは、Object.getPrototypeOf()やReflect.getPrototypeOf()、instanceofなどでプロトタイプが参照されるときに呼び出されます。
このハンドラーは、オブジェクトまたはnullを返す必要があります。
構文
getPrototypeOf: function( ターゲットオブジェクト )
実行例:
const proto = { toString:()=>"テストオブジェクト" };
const handle = {
getPrototypeOf:function( target ){
if( Object.isExtensible( target ) ) return proto;
return Reflect.getPrototypeOf( target);
}
}
const proxy1 = new Proxy( { } , handle );
console.log( Object.getPrototypeOf(proxy1).toString() ); // テストオブジェクト
const proxy2 = new Proxy( Object.freeze({ }) , handle );
console.log( Object.getPrototypeOf(proxy2).toString() ); // [object Object]
補足:
ターゲットオブジェクトがObject.freeze()などで変更不可となっている場合、実際と異なる値を返すとエラーになります。
const handle = {
getPrototypeOf:function( target ){
return proto;
}
}
const proxy2 = new Proxy( Object.freeze({ }) , handle );
console.log( Object.getPrototypeOf(proxy2).toString() );
// TypeError: proxy getPrototypeOf handler didn't return the target object's prototype
hasハンドラー
hasハンドラーはin演算子やReflect.has()の対象となったとき、呼び出されます。
このハンドラーは真偽値を返す必要があります。
構文
has: function( ターゲットオブジェクト , プロパティ名 )
実行例:
const proxy = new Proxy( { } , {
has:function ( target, property ){
return property.startsWith("prop");
}
});
console.log( "prop1" in proxy ); // true
console.log( "data1" in proxy ); // false
補足:
ターゲットオブジェクトにプロパティが存在していて、プロパティのconfigurableがfalseまたはオブジェクトがObject.freeze()などで変更不可になっている場合、ハンドラーがfalseを返すとエラーになります。
const proxy = new Proxy( Object.freeze({ data1:100 }) , {
has:function ( target, property ){
return false;
}
});
console.log( "prop1" in proxy ); // false
console.log( "data1" in proxy ); // TypeError: proxy can't report a non-configurable own property '"data1"' as non-existent
isExtensibleハンドラー
isExtensibleハンドラーは、Object.isExtensible()またはReflect.isExtensible()の対象となったときに呼び出されます。
このハンドラーは真偽値を返す必要があります。
構文
isExtensible: function( ターゲットオブジェクト )
実行例:
const proxy = new Proxy( Object.freeze({ }) , {
isExtensible:function ( target ){
return Reflect.isExtensible( target );
}
});
console.log( Object.isExtensible(proxy) ); // false
補足:
ハンドラーが、Object.isExtensible()またはReflect.isExtensible()の結果と異なる値を返すとエラーになります。
const proxy = new Proxy( Object.freeze({ }) , {
isExtensible:function ( target ){
return true;
}
});
console.log( Object.isExtensible(proxy) ); // TypeError: proxy must report same extensiblitity as target
ownKeysハンドラー
ownKeysハンドラーは、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.keys()、Object.entries()、Object.values()、Reflect.ownKeys()等で呼び出されます。
このハンドラーは、文字列とシンボル値のみを要素として持つ配列を返す必要があります。
構文
ownKeys: function( ターゲットオブジェクト )
実行例:
const obj = { prop:100 , [Symbol()]:"SYMBOL" };
const proxy = new Proxy( obj , {
ownKeys:function ( target ){
const result = Reflect.ownKeys( target );
result.push("addProp");
return result;
}
});
console.log( Object.getOwnPropertyNames(proxy) ); // Array [ "prop", "addProp" ]
console.log( Object.getOwnPropertySymbols(proxy) ); // Array [ Symbol() ]
console.log( Object.keys(proxy) ); // Array [ "prop" ] ← 存在するもののみ取得
console.log( Object.entries(proxy) ); // Array [ [ "prop", 100 ] ] ← 存在するもののみ取得
console.log( Object.values(proxy) ); // Array [ 100 ] ← 存在するもののみ取得
console.log( Reflect.ownKeys(proxy) ); // Array(3) [ "prop", Symbol(), "addProp" ]
※ownKeysハンドラーで返した値は、各メソッドで必要なもののみピックアップされます。
補足:
configurable属性がfalseのプロパティ名を返さないと、エラーになります。
const obj = Object.defineProperty( { prop:100 } , "prop2" , {
value:200
});
const proxy = new Proxy( obj , {
ownKeys:function ( target ){
return ["prop"];
}
});
console.log( Object.getOwnPropertyNames(proxy) );
// TypeError: proxy can't skip a non-configurable property '"prop2"'
また、ターゲットオブジェクトがObject.freeze()などで変更不可になっている場合、Reflect.ownKeys(ターゲットオブジェクト)の結果と異なる結果を返すとエラーになります。
preventExtensionsハンドラー
preventExtensionsハンドラーは、Object.preventExtensions()およびReflect.preventExtensions()の対象となったとき、呼び出されます。
このハンドラーは真偽値を返す必要があります。
構文
preventExtensions: function( ターゲットオブジェクト )
実行例:
const proxy = new Proxy( { prop:100 } , {
preventExtensions:function ( target ){
return Reflect.preventExtensions( target );
}
});
Object.preventExtensions(proxy);
補足1:
ハンドラーがfalseを返すとエラーになります。
補足2:
ターゲットオブジェクトがプロパティ追加可能な時にtrueを返すとエラーになります。
const handler = {
preventExtensions:function ( target ){
return true;
}
};
const proxy1 = new Proxy( {} , handler );
const proxy2 = new Proxy( Object.freeze( {} ) , handler );
console.log( Reflect.preventExtensions( proxy2 ) ); // true
console.log( Reflect.preventExtensions( proxy1 ) ); // TypeError: proxy can't report an extensible object as non-extensible
setハンドラー
setハンドラーは、プロパティが存在するしないにかかわらず、プロパティに値がセットされるときに呼び出されます。
このハンドラーは真偽値を返す必要があります。
構文
set: function( ターゲットオブジェクト , プロパティ名 , 値 , proxyオブジェクト )
実行例:
const proxy = new Proxy({ prop:100 } , {
set:function ( target, property , value , receiver ){
if( !(property in target) )
throw new ReferenceError( `存在しないプロパティ"${property}"に値をセットしようとしました` );
return Reflect.set( ...arguments );
}
} );
try{
proxy.prop = 200;
proxy.data = 200;
}catch (e) {
console.log( e ); // ReferenceError: 存在しないプロパティ"data"に値をセットしようとしました
}
補足:
setハンドラーがtrueを返し、プロパティのconfigurable属性がfalseのとき、次の条件を満たすとエラーになります。
- データプロパティ(ゲッター/セッターでない)でwritable属性がfalseのとき、セットしようとした値と実際の値が異なる
- アクセサープロパティでセッターが未定義
const obj = Object.defineProperties( { } , {
prop1 : { value:100 }, // ← configurable、writableがfalse
prop2 : { get:()=>{} } // ← セッターがない
});
const proxy = new Proxy( obj , { set:()=>true } );
try{
proxy.prop1 = 200;
}catch (e) {
console.log( e ); // TypeError: proxy can't successfully set a non-writable, non-configurable property '"prop1"'
}
try{
proxy.prop2 = 200;
}catch (e) {
console.log( e ); // TypeError: proxy can't succesfully set an accessor property '"prop2"' without a setter
}
setPrototypeOfハンドラー
setPrototypeOfハンドラーは、Object.setPrototypeOf()またはReflect.setPrototypeOf()の対象となったとき、呼び出されます。
このハンドラーは真偽値を返す必要があります。
構文
setPrototypeOf: function( ターゲットオブジェクト , プロトタイプオブジェクト )
実行例:
const proxy = new Proxy( {} , {
setPrototypeOf:function( target, prototype ) {
throw new TypeError("プロトタイプの変更はできません");
}
});
Object.setPrototypeOf( proxy , {} ); // TypeError: プロトタイプの変更はできません
※Proxyを使用しなくてもオブジェクトを拡張不可にすれば、エラーがスローされます。
Object.setPrototypeOf( Object.freeze({}) , {} ); // TypeError: can't set prototype of this object
備考:
ターゲットオブジェクトがObject.freeze()などで変更不可になっている場合、現在のプロトタイプと異なるオブジェクトでハンドラーが呼び出されるとエラーになります。
Proxyの仕組み
Proxyはオブジェクトの通常の処理をトラップしてカスタマイズできることから、内部で複雑な処理をおこなっている印象があります。
実際は、とても単純な仕組みだったりします。
ここでは、最も簡単な処理をおこなっているisExtensibleハンドラーで解説してみます。
Object.isExtensible()の処理
まずはObject.isExtensible()で、どのような処理が行われているか見てみます。
Object.isExtensible()は、オブジェクトが構成可能かどうか、つまりプロパティを追加できるかどうかを真偽値で返すメソッドです。
次のコードは、Object.isExtensible()の実際の処理の流れを、仮想的な関数で表したものです。
Object.isExtensible = function( obj ) {
return obj.[[IsExtensible]]();
}
オブジェクトはプログラムコードからアクセスできない[[IsExtensible]]という名前の内部メソッドを持っています。
[[IsExtensible]]を実行すると、オブジェクトが構成可能かどうかが真偽値で返ってきます。
実際の処理は、次のように内部プロパティ[[Extensible]]の値を返すだけです。
Objectインスタンス.[[IsExtensible]] = function( ) {
return this.[[Extensible]];
}
※わかりにくいですが[[IsExtensible]]と[[Extensible]]は異なるプロパティです
Proxyインスタンスの構造
Proxyインスタンスは[[ProxyTarget]]と[[ProxyHandler]]という、内部プロパティを持っています。
このプロパティには、インスタンス作成時に与えられた引数の値がセットされています。
const proxy = new Proxy( ターゲットオブジェクト , ハンドルオブジェクト);
proxy.[[ProxyTarget]]:ターゲットオブジェクト
proxy.[[ProxyHandler]]:ハンドルオブジェクト
さらに、通常のオブジェクトが持っている[[IsExtensible]]メソッドを所持しています。
そのためObject.isExtensible()にProxyインスタンスを渡すと、そのまま[[IsExtensible]]が実行されます。
Object.isExtensible = function( obj ) {
return obj.[[IsExtensible]](); // proxy.[[IsExtensible]]をそのまま実行できる
}
const proxy = new Proxy( obj , handle );
Object.isExtensible( proxy );
つまり呼び出し側のメソッドは、Proxyインスタンスのために特別な処理を実行しているのではなく、通常と同じように処理してるのです。
proxy.[[IsExtensible]]の処理
呼び出し側のメソッドは特別な処理をしていませんが、[[IsExtensible]]メソッドの処理内容は通常オブジェクトとProxyインスタンスで異なります。
比較しやすいように通常オブジェクトの[[IsExtensible]]メソッド処理を、もう一度記載しておきます。
Objectインスタンス.[[IsExtensible]] = function( ) {
return this.[[Extensible]];
}
非常にシンプルですね。
Proxyインスタンスの[[IsExtensible]]メソッドは次のような処理です。
proxy.[[IsExtensible]] = function(){
const targetObj = this.[[ProxyTarget]];
const handleObj = this.[[ProxyHandler]];
if( !handleObj.hasOwnProperty( "isExtensible" ) )
return targetObj.[[IsExtensible]]( );
const result = handleObj.isExtensible( );
// 結果の検証
const originalResult = targetObj.[[IsExtensible]]( );
if( result !== originalResult ) throw new TypeError( エラーメッセージ );
return result;
}
ハンドルオブジェクトにisExtensibleプロパティがなければターゲットオブジェクトの[[IsExtensible]]が実行され結果を返します。
isExtensibleプロパティがあるならハンドラー関数として実行されます。
そして検証がおこなわれます。
ここではターゲットオブジェクトの[[IsExtensible]]を実行して、その結果とハンドラー関数の結果を比較し、異なるならエラーを発生させています。
最後にハンドラー関数の結果を返して終了です。
ハンドラーと内部メソッドの関係
他のハンドラーもisExtensibleとほぼ同じ流れで処理されます。
ただしオブジェクト内には[[IsExtensible]]以外にも数多くの内部メソッドが存在していて、それぞれが各ハンドラーに対応しています。
次の表は内部メソッドとハンドラーの対応表です。
ハンドラー名 | 内部メソッド | 後処理の量 |
---|---|---|
apply | [[Call]] | なし |
construct | [[Construct]] | 少ない |
defineProperty | [[DefineOwnProperty]] | 多い |
deleteProperty | [[Delete]] | 少し多い |
get | [[Get]] | 少し多い |
getOwnPropertyDescriptor | [[DefineOwnProperty]] | 少し多い |
Reflect.getPrototypeOf | [[GetPrototypeOf]] | 少し多い |
has | [[HasProperty]] | 少し多い |
isExtensible | [[IsExtensible]] | 少ない |
ownKeys | [[GetOwnProperty]] | 多い |
preventExtensions | [[PreventExtensions]] | 少ない |
set | [[Set]] | 少し多い |
setPrototypeOf | [[SetPrototypeOf]] | 少ない |
表中の後処理の量は、内部メソッドのアルゴリズムのステップ数を見ての主観的な印象です。
実際の処理時間を計測したものではありません。
また『多い』となっていても体感的には一瞬で終了するので、それほど気にする必要がありません。
ただし[[Get]]と[[Set]]はオブジェクト内プロパティ値を入出力する度に呼び出されます。
そのため頻繁にアクセスが行われると、場合によっては目に見えた速度低下がおこる可能性があります。
速度が気になるときはgetおよびsetハンドラーの処理時間をできる限り少なくするか、Proxyを使用しない方法を考える必要があります。
更新日:2024/04/26
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。