MENU

JavaScriptオブジェクトリフレクション

【JavaScript】 Proxyオブジェクトの使い方と仕組み

更新日:2021/12/11

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.prop = 100;
proxyObj.func = ()=>{};

console.log( proxyObj ); // Proxy { <target>: {…}, <handler>: {} } ←変化なし
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関数( )
constructnew 関数( )
definePropertyObject.defineProperty( )
deletePropertydelete オブジェクト.プロパティ
getオブジェクト.プロパティ または オブジェクト[ プロパティ ]
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor( )
Reflect.getPrototypeOfObject.getPrototypeOf( )
hasプロパティ in オブジェクト
isExtensibleObject.isExtensible( )
ownKeysObject.getOwnPropertyNames( ) , Object.getOwnPropertySymbols( )
preventExtensionsObject.preventExtensions( )
setオブジェクト.プロパティ = 値 または オブジェクト[ プロパティ ] = 値
setPrototypeOfObject.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( `結果: ${ Reflect.apply( ...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( `結果: ${ Reflect.apply( ...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ハンドラーでfalseをリターンすると、エラーがスローされます。
trueをリターンすると、内部で次のような検証がおこなわれます。

 

■definePropertyハンドラー実行後の検証内容

 

※ここではターゲットオブジェクトをobjとし、指定したプロパティ記述子を記述子としています。

 

  1. objにプロパティが無い場合(ハンドラーで何も処理せずにtrueを返したときなど)
    1. Object.freeze()などでobjが変更不可になっているなら、エラーがスローされます。
    2. 記述子にconfigurable属性があり、値がfalseのときエラーがスローされます。configurableの規定値はfalseですが、記述子に設定がなければエラーになりません(理由不明)
  2. objにプロパティがある場合(変更後のobjからプロパティ記述子が取得されます。ここでは変更後記述子とします)
    1. 記述子と変更後記述子のconfigurable属性が異なる場合、エラーがスローされます。
    2. 記述子にconfigurable属性があり、記述子と変更後記述子のenumerable属性が異なる場合、エラーがスローされます。
    3. 記述子と変更後記述子共にデータ記述子(ゲッター/セッターでない)でなく、変更後記述子のconfigurablewritablefalseのとき
      1. 記述子にwritableがあり値がtrueなら、エラーがスローされます。
      2. 記述子にvalueがあり、変更後記述子のvalueと異なるなら、エラーがスローされます。

    4. 記述子と変更後記述子共にアクセサー記述子(ゲッター/セッター)で、変更後記述子のconfigurablefalseのとき
      1. 記述子にsetがあり、変更後記述子のsetと異なるなら、エラーがスローされます。
      2. 記述子にgetがあり、変更後記述子のgetと異なるなら、エラーがスローされます。

    5. 記述子と変更後記述子のタイプが異なり、変更後記述子のconfigurablefalseなら、エラーがスローされます。
    6. 記述子にconfigurableがあり値がfalseで、変更後記述子のconfigurabletrueなら、エラーがスローされます。
    7. 変更後記述子がデータ記述子(ゲッター/セッターでない)でconfigurablefalsewritabletrueのとき、記述子にwritableがあり値がfalseなら、エラーがスローされます。

 

とりあえず、テクニカルなことをやろうとするとエラーになる可能性が高いと思っておくといいですね。

 

 

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を使用しない方法を考える必要があります。

 

 

最後までお読みいただきありがとうございます

記事の内容について

こんにちはけーちゃんです。
説明するのって難しいですね。


「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

【お願い】

お願い

■このページのURL


■このページのタイトル


■リンクタグ


※リンクして頂いた方でご希望者には貴サイトの紹介記事を作成してリンクを設置します。
サイト上部の問い合わせよりご連絡ください。
ただしサイトのジャンルによっては、お断りさせていただくことがあります。