Proxy
オブジェクトは、別のオブジェクトをラップし、プロパティの読み書きなどの操作をインターセプトし、必要に応じて自身で処理するか、またはオブジェクトに透過的に処理させることができます。
Proxyは多くのライブラリやブラウザフレームワークで使用されています。この記事では、多くの実用的なアプリケーションを見ていきます。
Proxy
構文
let proxy = new Proxy(target, handler)
target
– ラップするオブジェクト。関数を含め、何でも可能です。handler
– プロキシ構成。"トラップ"、操作をインターセプトするメソッドを持つオブジェクトです。– 例:target
のプロパティを読み取るためのget
トラップ、target
にプロパティを書き込むためのset
トラップなど。
proxy
に対する操作では、handler
に対応するトラップがある場合、それが実行され、プロキシがそれを処理する機会を得ます。そうでない場合、操作はtarget
上で実行されます。
開始例として、トラップなしでプロキシを作成してみましょう。
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 5; // writing to proxy (1)
alert(target.test); // 5, the property appeared in target!
alert(proxy.test); // 5, we can read it from proxy too (2)
for(let key in proxy) alert(key); // test, iteration works (3)
トラップがないため、proxy
に対するすべての操作はtarget
に転送されます。
- 書き込み操作
proxy.test=
はtarget
に値を設定します。 - 読み取り操作
proxy.test
はtarget
から値を返します。 proxy
に対する反復処理は、target
から値を返します。
ご覧のように、トラップがない場合、proxy
はtarget
の透過的なラッパーです。
Proxy
は特別な「エキゾチックオブジェクト」です。独自のプロパティを持っていません。空のhandler
を使用すると、操作を透過的にtarget
に転送します。
より多くの機能をアクティブにするには、トラップを追加してみましょう。
トラップで何をインターセプトできるのでしょうか?
オブジェクトに対するほとんどの操作には、JavaScriptの仕様で、最低レベルでどのように動作するかを記述した「内部メソッド」と呼ばれるものがあります。たとえば、プロパティを読み取るための内部メソッド[[Get]]
、プロパティを書き込むための内部メソッド[[Set]]
などがあります。これらのメソッドは仕様でのみ使用され、名前で直接呼び出すことはできません。
Proxyトラップは、これらのメソッドの呼び出しをインターセプトします。これらは、Proxy仕様および下の表にリストされています。
すべての内部メソッドに対して、この表にトラップがあります。操作をインターセプトするために、new Proxy
のhandler
パラメーターに追加できるメソッドの名前です。
内部メソッド | ハンドラーメソッド | 次のときにトリガーされる |
---|---|---|
[[Get]] |
get |
プロパティを読み取る |
[[Set]] |
set |
プロパティに書き込む |
[[HasProperty]] |
has |
in 演算子 |
[[Delete]] |
deleteProperty |
delete 演算子 |
[[Call]] |
apply |
関数呼び出し |
[[Construct]] |
construct |
new 演算子 |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty、Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor、for..in 、Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames、Object.getOwnPropertySymbols、for..in 、Object.keys/values/entries |
JavaScriptは、不変条件と呼ばれる、内部メソッドとトラップによって満たされなければならない条件を強制します。
それらのほとんどは戻り値用です。
[[Set]]
は、値が正常に書き込まれた場合はtrue
を返し、そうでない場合はfalse
を返す必要があります。[[Delete]]
は、値が正常に削除された場合はtrue
を返し、そうでない場合はfalse
を返す必要があります。- …以下に例を挙げます。
他にもいくつかの不変条件があります。
- プロキシオブジェクトに適用される
[[GetPrototypeOf]]
は、プロキシオブジェクトのターゲットオブジェクトに適用される[[GetPrototypeOf]]
と同じ値を返す必要があります。言い換えれば、プロキシのプロトタイプを読み取ることは、常にターゲットオブジェクトのプロトタイプを返す必要があります。
トラップはこれらの操作をインターセプトできますが、これらのルールに従う必要があります。
不変条件は、言語機能の正確で一貫した動作を保証します。完全な不変条件のリストは仕様にあります。何かおかしなことをしていない限り、おそらくそれらに違反することはないでしょう。
それが実際どのように機能するかを見てみましょう。
「get」トラップによるデフォルト値
最も一般的なトラップは、プロパティの読み取り/書き込み用です。
読み取りをインターセプトするには、handler
にメソッドget(target, property, receiver)
が必要です。
プロパティが読み取られると、次の引数でトリガーされます。
target
– ターゲットオブジェクト。new Proxy
に最初の引数として渡されたオブジェクトです。property
– プロパティ名。receiver
– ターゲットプロパティがゲッターの場合、receiver
は、その呼び出しでthis
として使用されるオブジェクトです。通常、それはproxy
オブジェクト自体(またはプロキシから継承する場合はプロキシから継承したオブジェクト)です。今のところ、この引数は必要ないため、後で詳しく説明します。
get
を使用して、オブジェクトのデフォルト値を実装してみましょう。
存在しない値に対して0
を返す数値配列を作成します。
通常、存在しない配列項目を取得しようとすると、undefined
が取得されますが、読み取りをトラップし、そのようなプロパティがない場合は0
を返すプロキシに通常の配列をラップします。
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // default value
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such item)
ご覧のように、get
トラップを使用すると非常に簡単に行うことができます。
Proxy
を使用して、「デフォルト」値の任意のロジックを実装できます。
フレーズとその翻訳を含む辞書があると想像してください。
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
現時点では、フレーズがない場合、dictionary
からの読み取りはundefined
を返します。しかし実際には、フレーズを翻訳されていないままにすることは、通常、undefined
よりも優れています。したがって、undefined
の代わりに、その場合は翻訳されていないフレーズを返すようにします。
それを実現するために、読み取り操作をインターセプトするプロキシでdictionary
をラップします。
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercept reading a property from dictionary
if (phrase in target) { // if we have it in the dictionary
return target[phrase]; // return the translation
} else {
// otherwise, return the non-translated phrase
return phrase;
}
}
});
// Look up arbitrary phrases in the dictionary!
// At worst, they're not translated.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation)
プロキシが変数をどのように上書きしているかにご注意ください。
dictionary = new Proxy(dictionary, ...);
プロキシは、ターゲットオブジェクトを完全に置き換える必要があります。プロキシされた後、ターゲットオブジェクトを参照するべきではありません。そうしないと、簡単に混乱してしまう可能性があります。
「set」トラップによる検証
数値専用の配列が必要だとします。別の型の値が追加された場合は、エラーが発生する必要があります。
set
トラップは、プロパティが書き込まれるときにトリガーされます。
set(target, property, value, receiver)
:
target
– ターゲットオブジェクト。new Proxy
に最初の引数として渡されたオブジェクトです。property
– プロパティ名。value
– プロパティ値。receiver
–get
トラップと同様に、セッタープロパティでのみ問題になります。
設定が成功した場合は、set
トラップはtrue
を返し、そうでない場合はfalse
(TypeError
をトリガーします)を返す必要があります。
それを使用して新しい値を検証してみましょう。
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // to intercept property writing
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // added successfully
numbers.push(2); // added successfully
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("This line is never reached (error in the line above)");
ご注意ください。配列の組み込み機能はまだ機能しています。値はpush
で追加されます。値が追加されると、length
プロパティは自動的に増加します。プロキシは何も壊しません。
push
やunshift
などの値を追加する配列メソッドをオーバーライドして、そこにチェックを追加する必要はありません。なぜなら、内部的にはプロキシによってインターセプトされる[[Set]]
操作を使用するからです。
そのため、コードはクリーンで簡潔です。
true
を返す上記のように、保持される不変条件があります。
set
の場合、正常な書き込みの場合はtrue
を返す必要があります。
それを忘れたり、偽の値を返したりすると、操作によってTypeError
が発生します。
「ownKeys」および「getOwnPropertyDescriptor」を使用した反復
Object.keys
、for..in
ループ、およびオブジェクトプロパティを反復処理する他のほとんどのメソッドは、プロパティのリストを取得するために[[OwnPropertyKeys]]
内部メソッド(ownKeys
トラップによってインターセプトされる)を使用します。
このようなメソッドは詳細が異なります。
Object.getOwnPropertyNames(obj)
は、シンボルではないキーを返します。Object.getOwnPropertySymbols(obj)
は、シンボルキーを返します。Object.keys/values()
は、enumerable
フラグ付きのシンボルではないキー/値を返します(プロパティフラグは、記事プロパティフラグとディスクリプタで説明しました)。for..in
は、enumerable
フラグ付きのシンボルではないキー、およびプロトタイプキーをループ処理します。
…しかし、すべてはそのリストから始まります。
以下の例では、ownKeys
トラップを使用して、for..in
ループがuser
を反復処理し、さらにObject.keys
とObject.values
がアンダースコア_
で始まるプロパティをスキップするようにします。
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
今のところ、これは機能しています。
ただし、オブジェクトに存在しないキーを返した場合、Object.keys
はそれをリストアップしません。
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
なぜでしょうか?理由は簡単です。Object.keys
は、enumerable
フラグを持つプロパティのみを返します。これをチェックするために、すべてのプロパティに対して内部メソッド[[GetOwnProperty]]
を呼び出し、その記述子を取得します。そしてここで、プロパティが存在しないため、その記述子は空であり、enumerable
フラグがないため、スキップされます。
Object.keys
がプロパティを返すには、プロパティがenumerable
フラグ付きでオブジェクトに存在するか、[[GetOwnProperty]]
への呼び出し(トラップgetOwnPropertyDescriptor
がそれを行います)をインターセプトして、enumerable: true
を持つ記述子を返す必要があります。
以下はその例です。
let user = { };
user = new Proxy(user, {
ownKeys(target) { // called once to get a list of properties
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
もう一度注意しておきましょう。[[GetOwnProperty]]
をインターセプトする必要があるのは、プロパティがオブジェクトに存在しない場合のみです。
「deleteProperty」およびその他のトラップによる保護されたプロパティ
アンダースコア_
で始まるプロパティとメソッドは内部用であるという広範な慣習があります。これらはオブジェクトの外部からアクセスすべきではありません。
ただし、技術的には可能です。
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
プロキシを使用して、_
で始まるプロパティへのアクセスをすべて防止しましょう。
以下のトラップが必要です。
- そのようなプロパティを読み取る際にエラーをスローする
get
、 - 書き込み時にエラーをスローする
set
、 - 削除時にエラーをスローする
deleteProperty
、 for..in
やObject.keys
のようなメソッドから_
で始まるプロパティを除外するownKeys
。
以下にコードを示します。
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" doesn't allow to write _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" doesn't allow to delete _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" filters out _password
for(let key in user) alert(key); // name
get
トラップの(*)
の行にある重要な詳細に注意してください。
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
なぜvalue.bind(target)
を呼び出す関数が必要なのでしょうか?
理由は、user.checkPassword()
のようなオブジェクトメソッドが_password
にアクセスできるようにする必要があるからです。
user = {
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
user.checkPassword()
への呼び出しは、this
としてプロキシ化されたuser
を取得します(ドットの前にあるオブジェクトがthis
になります)。したがって、this._password
にアクセスしようとすると、get
トラップがアクティブになり(任意のプロパティの読み取りでトリガーされます)、エラーがスローされます。
そこで、オブジェクトメソッドのコンテキストを元のオブジェクトtarget
に(*)
の行でバインドします。その後、将来の呼び出しではトラップなしでthis
としてtarget
が使用されます。
その解決策は通常は機能しますが、理想的ではありません。メソッドがプロキシ化されていないオブジェクトをどこか別の場所に渡してしまうと、元のオブジェクトがどこにあり、プロキシ化されたオブジェクトがどこにあるのかが混乱する可能性があります。
また、オブジェクトは複数回プロキシ化される可能性があり(複数のプロキシがオブジェクトに異なる「調整」を追加する可能性があります)、アンラップされたオブジェクトをメソッドに渡すと、予期しない結果が発生する可能性があります。
したがって、このようなプロキシはどこでも使用すべきではありません。
最新のJavaScriptエンジンは、クラスで#
で始まるプライベートプロパティをネイティブにサポートしています。これらは、プライベートおよび保護されたプロパティとメソッドの記事で説明されています。プロキシは必要ありません。
ただし、このようなプロパティには独自の問題があります。特に、これらは継承されません。
「has」トラップを使用した「範囲内」
さらに例を見てみましょう。
範囲オブジェクトがあるとします。
let range = {
start: 1,
end: 10
};
in
演算子を使用して、数値がrange
内にあるかどうかを確認したいと思います。
has
トラップはin
呼び出しをインターセプトします。
has(target, property)
target
–new Proxy
に最初の引数として渡されたターゲットオブジェクトです。property
– プロパティ名
以下にデモを示します。
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
素晴らしい構文糖衣ですね。そして非常に簡単に実装できます。
関数のラップ:「apply」
関数をプロキシでラップすることもできます。
apply(target, thisArg, args)
トラップは、関数としてプロキシを呼び出す処理を行います。
target
はターゲットオブジェクト(JavaScriptでは関数はオブジェクトです)です。thisArg
はthis
の値です。args
は引数のリストです。
たとえば、デコレータと転送、call/applyの記事で行ったdelay(f, ms)
デコレータを思い出してみましょう。
その記事では、プロキシを使用せずにそれを行いました。delay(f, ms)
を呼び出すと、ms
ミリ秒後にすべての呼び出しをf
に転送する関数が返されました。
以下は以前の関数ベースの実装です。
function delay(f, ms) {
// return a wrapper that passes the call to f after the timeout
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// after this wrapping, calls to sayHi will be delayed for 3 seconds
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
すでに見たように、これはほとんどの場合機能します。ラッパー関数(*)
は、タイムアウト後に呼び出しを実行します。
ただし、ラッパー関数はプロパティの読み取り/書き込み操作やその他の操作を転送しません。ラッピング後、name
、length
などの元の関数のプロパティへのアクセスが失われます。
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
Proxy
は、すべての操作をターゲットオブジェクトに転送するため、はるかに強力です。
ラッパー関数の代わりにProxy
を使用してみましょう。
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)
結果は同じですが、これで呼び出しだけでなく、プロキシに対するすべての操作が元の関数に転送されます。したがって、sayHi.length
は(*)
の行のラッピング後も正しく返されます。
より「リッチ」なラッパーを取得しました。
他のトラップも存在します。完全なリストはこの記事の冒頭にあります。それらの使用パターンは上記のものと似ています。
Reflect
Reflect
は、Proxy
の作成を簡素化する組み込みオブジェクトです。
以前に、[[Get]]
、[[Set]]
などの内部メソッドは仕様のみであり、直接呼び出すことはできないと述べました。
Reflect
オブジェクトは、それをいくらか可能にします。そのメソッドは、内部メソッドの最小限のラッパーです。
以下は、同じことを行う操作とReflect
呼び出しの例です。
操作 | Reflect 呼び出し |
内部メソッド |
---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
… | … | … |
例として
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
特に、Reflect
を使用すると、演算子(new
、delete
…)を関数(Reflect.construct
、Reflect.deleteProperty
、…)として呼び出すことができます。これは興味深い機能ですが、ここでは別のことが重要です。
Proxy
でトラップ可能なすべての内部メソッドに対して、Reflect
には対応するメソッドがあり、Proxy
トラップと同じ名前と引数を持っています。
したがって、Reflect
を使用して操作を元のオブジェクトに転送できます。
この例では、両方のトラップget
とset
が、メッセージを表示しながら、読み取り/書き込み操作をオブジェクトに透過的に(まるで存在しないかのように)転送します。
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
ここでは
Reflect.get
はオブジェクトプロパティを読み取ります。Reflect.set
はオブジェクトプロパティを書き込み、成功した場合はtrue
、それ以外の場合はfalse
を返します。
つまり、すべてがシンプルです。トラップが呼び出しをオブジェクトに転送したい場合は、同じ引数でReflect.<method>
を呼び出すだけで十分です。
ほとんどの場合、Reflect
なしで同じことができます。たとえば、プロパティReflect.get(target, prop, receiver)
の読み取りをtarget[prop]
で置き換えることができます。ただし、重要なニュアンスがあります。
ゲッターのプロキシ
Reflect.get
が優れている理由を示す例を見てみましょう。また、以前は使用しなかったget/set
が3番目の引数receiver
を持っている理由も見てみましょう。
_name
プロパティとそのゲッターを持つオブジェクトuser
があるとします。
以下は、その周囲のプロキシです。
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
get
トラップはここで「透過的」であり、元のプロパティを返し、他に何も実行しません。これは、私たちの例では十分です。
すべて問題ないように見えます。しかし、例をもう少し複雑にしてみましょう。
別のオブジェクトadmin
をuser
から継承した後、正しくない動作を確認できます。
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
admin.name
を読み取ると、"Guest"
ではなく"Admin"
が返されるはずです。
何が問題なのでしょうか?おそらく継承で何か間違ったことをしたのでしょうか?
ただし、プロキシを削除すると、すべてが期待どおりに機能します。
問題は実際にはプロキシ、つまり(*)
の行にあります。
-
admin.name
を読み取ると、admin
オブジェクトにはそのような独自のプロパティがないため、検索はそのプロトタイプに移動します。 -
プロトタイプは
userProxy
です。 -
プロキシから
name
プロパティを読み取るとき、そのget
トラップがトリガーされ、(*)
の行でtarget[prop]
として元のオブジェクトからそれを返します。prop
がゲッターの場合、target[prop]
を呼び出すと、コンテキストthis=target
でそのコードが実行されます。したがって、結果は元のオブジェクトtarget
からのthis._name
、つまりuser
からのものです。
このような状況を修正するには、get
トラップの3番目の引数であるreceiver
が必要です。これは、ゲッターに渡される正しいthis
を保持します。この例では、それがadmin
です。
ゲッターのコンテキストを渡すにはどうすればよいでしょうか?通常の関数の場合、call/apply
を使用できますが、ゲッターは「呼び出される」のではなく、アクセスされるだけです。
Reflect.get
はそれを行うことができます。それを使用すると、すべてが正しく機能します。
以下は修正されたバリアントです。
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
これで、正しいthis
(つまりadmin
)への参照を保持するreceiver
が、(*)
の行でReflect.get
を使用してゲッターに渡されます。
トラップをさらに短く書き直すことができます。
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect
呼び出しはトラップとまったく同じように名前が付けられ、同じ引数を受け入れます。これらはこのように特別に設計されました。
したがって、return Reflect...
は、操作を転送し、それに関連するものを忘れないようにするための、安全で簡単な方法を提供します。
プロキシの制限
プロキシは、既存のオブジェクトの動作を最下位レベルで変更または調整する独自の方法を提供します。それでも、完璧ではありません。制限事項があります。
組み込みオブジェクト:内部スロット
Map
、Set
、Date
、Promise
などの多くの組み込みオブジェクトは、いわゆる「内部スロット」を使用します。
これらはプロパティのようなものですが、内部の、仕様のみの目的のために予約されています。たとえば、Map
は内部スロット[[MapData]]
に項目を格納します。組み込みメソッドは、内部メソッド[[Get]]/[[Set]]
を介してではなく、直接アクセスします。したがって、Proxy
はそれをインターセプトできません。
なぜ気にするのですか?結局のところ、それらは内部的なものです!
さて、ここに問題があります。そのような組み込みオブジェクトがプロキシ化された後、プロキシにはこれらの内部スロットがないため、組み込みメソッドは失敗します。
例として
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
内部的には、Map
はすべてのデータをその[[MapData]]
内部スロットに格納します。プロキシにはそのようなスロットはありません。組み込みメソッドMap.prototype.set
メソッドは、内部プロパティthis.[[MapData]]
にアクセスしようとしますが、this=proxy
であるため、proxy
内で見つけることができず、失敗します。
幸いなことに、それを修正する方法があります。
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
これで、get
トラップがmap.set
などの関数プロパティをターゲットオブジェクト(map
)自体にバインドするため、正常に機能します。
前の例とは異なり、proxy.set(...)
内のthis
の値はproxy
ではなく、元のmap
になります。したがって、set
の内部実装がthis.[[MapData]]
内部スロットにアクセスしようとすると、成功します。
Array
には内部スロットがありません注目すべき例外:組み込みのArray
は内部スロットを使用しません。これは、それが非常に昔に登場したため、歴史的な理由によります。
したがって、配列をプロキシ化するときには、そのような問題はありません。
プライベートフィールド
同様のことが、クラスのプライベートフィールドでも発生します。
例えば、getName()
メソッドはプライベートな #name
プロパティにアクセスしますが、プロキシ化の後で失敗します。
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
その理由は、プライベートフィールドが内部スロットを使って実装されているためです。JavaScriptは、それらにアクセスする際に [[Get]]/[[Set]]
を使用しません。
getName()
の呼び出しにおいて、this
の値はプロキシ化された user
であり、プライベートフィールドを持つスロットを持っていません。
再度になりますが、メソッドをバインドする解決策はそれを機能させます。
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
ただし、前述したように、この解決策には欠点があります。それは、元のオブジェクトをメソッドに公開してしまい、それがさらに渡されて他のプロキシ化された機能を壊す可能性があるということです。
プロキシ != ターゲット
プロキシと元のオブジェクトは異なるオブジェクトです。それは自然なことですよね?
したがって、元のオブジェクトをキーとして使用し、それをプロキシ化した場合、プロキシは見つけることができません。
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
ご覧のとおり、プロキシ化後、user
はセット allUsers
の中で見つけることができません。なぜなら、プロキシは異なるオブジェクトだからです。
===
をインターセプトできません。プロキシは、new
(construct
を使用)、in
(has
を使用)、delete
(deleteProperty
を使用) など、多くの演算子をインターセプトできます。
しかし、オブジェクトの厳密等価性テストをインターセプトする方法はありません。オブジェクトは自分自身とのみ厳密に等しく、他のどの値とも等しくありません。
そのため、オブジェクトの等価性を比較するすべての操作と組み込みクラスは、オブジェクトとプロキシを区別します。ここには透過的な置き換えはありません。
取消可能プロキシ
取消可能 プロキシは、無効にできるプロキシです。
たとえば、リソースを持っていて、いつでもアクセスを閉じたいとしましょう。
私たちができることは、トラップなしでそれを取消可能プロキシにラップすることです。このようなプロキシは、操作をオブジェクトに転送し、いつでも無効にすることができます。
構文は次のとおりです。
let {proxy, revoke} = Proxy.revocable(target, handler)
この呼び出しは、プロキシを無効にするための proxy
と revoke
関数を持つオブジェクトを返します。
以下に例を示します。
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// pass the proxy somewhere instead of object...
alert(proxy.data); // Valuable data
// later in our code
revoke();
// the proxy isn't working any more (revoked)
alert(proxy.data); // Error
revoke()
を呼び出すと、プロキシからターゲットオブジェクトへのすべての内部参照が削除されるため、それらはもはや接続されません。
当初、revoke
は proxy
とは分離しているので、revoke
を現在のスコープに残したまま proxy
を渡すことができます。
proxy.revoke = revoke
を設定することにより、revoke
メソッドをプロキシにバインドすることもできます。
別のオプションは、proxy
をキーとし、対応する revoke
を値とする WeakMap
を作成することです。これにより、プロキシの revoke
を簡単に見つけることができます。
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..somewhere else in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error (revoked)
ここでは、ガベージコレクションをブロックしないため、Map
ではなく WeakMap
を使用します。プロキシオブジェクトが「到達不能」になった場合(たとえば、どの変数もそれを参照しなくなった場合)、WeakMap
を使用すると、もはや必要なくなった revoke
とともにメモリから消去できます。
参考文献
概要
Proxy
はオブジェクトの周りのラッパーであり、オブジェクトに対する操作をオブジェクトに転送し、オプションでそれらの一部をトラップします。
クラスや関数を含む、あらゆる種類のオブジェクトをラップできます。
構文は次のとおりです。
let proxy = new Proxy(target, {
/* traps */
});
…それなら、target
の代わりに常に proxy
を使用する必要があります。プロキシには独自のプロパティやメソッドはありません。トラップが提供されている場合は操作をトラップし、それ以外の場合は target
オブジェクトに転送します。
トラップできるもの
- プロパティ(存在しないものも含む)の読み取り (
get
)、書き込み (set
)、削除 (deleteProperty
)。 - 関数の呼び出し (
apply
トラップ)。 new
演算子 (construct
トラップ)。- その他多くの操作(完全なリストは、記事の冒頭と ドキュメントにあります)。
これにより、「仮想」プロパティとメソッドを作成したり、デフォルト値、監視可能なオブジェクト、関数デコレーターなどを実装したりできます。
また、さまざまな機能の側面でデコレートするために、オブジェクトを異なるプロキシで複数回ラップすることもできます。
Reflect APIは、Proxy を補完するように設計されています。任意の Proxy
トラップに対して、同じ引数を持つ Reflect
の呼び出しがあります。ターゲットオブジェクトへの呼び出しを転送するために、それらを使用する必要があります。
プロキシにはいくつかの制限があります。
- 組み込みオブジェクトには「内部スロット」があり、それらへのアクセスをプロキシ化することはできません。上記の回避策を参照してください。
- プライベートクラスフィールドについても同様です。これらは内部的にスロットを使用して実装されるためです。したがって、プロキシ化されたメソッド呼び出しは、それらにアクセスするために
this
としてターゲットオブジェクトを持っている必要があります。 - オブジェクトの等価性テスト
===
をインターセプトすることはできません。 - パフォーマンス:ベンチマークはエンジンによって異なりますが、一般的に、最も単純なプロキシを使用してプロパティにアクセスすると、数倍長くなります。実際には、一部の「ボトルネック」オブジェクトでのみ問題になります。
コメント
<code>
タグを使用し、数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス (plnkr, jsbin, codepen…) を使用してください。