一.简介
new Proxy(target, handler) 構文は、ハンドラオブジェクト handler がターゲットオブジェクト target のプロパティアクセス方法をインターセプトすることを許可します
プロキシ機制はオブジェクトの 14 の内部メソッドの書き換えをサポートします。例えば [[Get]](key, receiver)、[[Set]](key, value, receiver) などです。その中で receiver は最初に検索を開始するオブジェクトです(アクセスしたいプロパティはプロトタイプチェーン上にある可能性があります)
まず proxy を作成します:
var obj = {
key: 'value'
};
var handler = {
get: function(target, key, receiver) {
console.log(`proxy key = ${key}`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(`proxy key = ${key}, value = ${value}`);
return Reflect.set(target, key, value, receiver);
}
// , apply
// , construct
// , defineProperty
// , deleteProperty
// , enumerate
// , getOwnPropertyDescriptor
// , getPrototypeOf
// , has
// , isExtensible
// , ownKeys
// , preventExtensions
// , setPrototypeOf
};
var proxy = new Proxy(obj, handler);
その後試してみます:
// test
// ターゲットオブジェクト obj から初期状態をコピー
console.log(proxy); // Object { key: "value" }
proxy.a = 1; // proxy key = a, value = 1
proxy['b'] = 2; // proxy key = b, value = 2
proxy.b++;
// log print:
// proxy key = b
// proxy key = b, value = 3
// プロキシオブジェクト proxy の状態はターゲットオブジェクト proxy と完全に一致
// すべての内部メソッドが転送されるため
console.log(`proxy.key = ${proxy.key}, proxy.a = ${proxy.a}, proxy.b = ${proxy.b}`);
console.log(`obj.key = ${obj.key}, obj.a = ${obj.a}, obj.b = ${obj.b}`);
// log print:
// proxy key = key
// proxy key = a
// proxy key = b
// proxy.key = value, proxy.a = 1, proxy.b = 3
// obj.key = value, obj.a = 1, obj.b = 3
// 逆向
obj.x = 2;
console.log(`obj.x = ${obj.x}, proxy.x = ${proxy.x}`);
// log print:
// proxy key = x
// obj.x = 2, proxy.x = 2
参照をコピーする(var proxy = obj;)のと同じように、obj と proxy の状態は完全に一致し、双方向同期です。唯一の違いは handler がこっそり何かをしたことです
二.特点
###1. 行為転送と状態同期
プロキシの行為:プロキシのすべての内部メソッドをターゲットに転送
プロキシオブジェクト proxy とターゲットオブジェクト target は状態を一致させます:プロキシオブジェクト作成時に target から現在の状態をコピーし、内部メソッドはすべて転送されます
###2.proxy !== target
例えば target が DOMElement で、proxy は異なります。document.body.appendChild(proxy) を呼び出すと TypeError がトリガーされます
しかも、上記の例では
console.log(obj === proxy); // false
console.log(obj == proxy); // false
これは非常に合理的です。プロキシは本来参照コピーとは異なるからです
###3.handler インターセプト
handler オブジェクトを通じて 14 の内部メソッドを書き換え、target のデフォルト行為をインターセプトして変更できます
handler によってインターセプトされない(handler オブジェクト中で書き換えられない)内部メソッドは直接ターゲットを指します
14 の内部メソッドはすべて handler オブジェクト中で書き換え可能です。プロパティ名は Reflect オブジェクトの 14 のプロパティ名と一致します。詳細は MDN
###4. プロキシ関係は解除可能
プロキシは解除可能です。Proxy.revocable(target, handler) を使用して Object { proxy: Object, revoke: revoke() } を作成し、revoke() を呼び出してプロキシ関係を解除します。解除後プロキシオブジェクトにアクセスするとエラーが発生します。例えば:
// 解除代理関係
var rProxy = Proxy.revocable({}, {
set: function(target, key, value, receiver) {
return Reflect.set(target, key, 0, receiver);
}
});
var p = rProxy.proxy;
p.a = 123;
console.log(p.a); // 0
// 解除代理
rProxy.revoke();
// p.b = 213; // 报错 TypeError: illegal operation attempted on a revoked proxy
###5. オブジェクト不変性
target が拡張不可能でない限り、プロキシオブジェクトは拡張不可能と宣言できません
三.应用场景
###1. 自動填充オブジェクト
// 1.自动填充
var Tree = function() {
return new Proxy({}, {
get: function(target, key, receiver) {
if (typeof target[key] === 'undefined') {
console.log(`${key} is undef, but it will be auto filled`);
// 利用当前 handler 创建 proxy 作为 value
Reflect.set(target, key, new Proxy({}, this), receiver);
// 上面代码等价于
// target[key] = new Proxy({}, this);
// 也等价于
// target[key] = Tree();
}
return Reflect.get(target, key, receiver);
}
});
}
var tree = Tree();
console.log(tree); // Object { branch1: Object }
tree.branch1.branch2.twig = 'green';
// log print:
// branch1 is undef, but it will be auto filled
// branch2 is undef, but it will be auto filled
console.log(tree); // Object { branch1: Object }
tree.branch1.branch3.twig = "yellow";
// log print:
// branch3 is undef, but it will be auto filled
console.log(tree); // Object { branch1: Object }
有用かどうかは別として、ES6 以前の時代には確かに実装できませんでした自動填充オブジェクト(ソースコードを「コンパイル」する黒魔術を除く)
P.S.上記の log 結果は Firefox47 からのものです。Chrome51.0.2704.106 では奇妙なものが現れました:
// chrome log print:
Object {splice: Object}
splice is undef, but it will be auto filled
splice is undef, but it will be auto filled
branch1 is undef, but it will be auto filled
branch2 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}
splice is undef, but it will be auto filled
branch3 is undef, but it will be auto filled
Object {splice: Object, branch1: Object}
splice はどこから来たのでしょうか?しかも splice の値 Object を確認すると、無限に展開できることがわかります(console は非常に長く広い > 形を表示します)。splice が自身を循環参照しているように感じられます。とりあえず Chrome 実装上のbug と考え、深く追求する必要はありません
###2. 改ざん防止オブジェクト
プロキシ作成には非常に注意が必要です。否则漏洞があります
function readOnly(obj) {
var err = new Error('can\'t modify read-only object');
return new Proxy(obj, {
// 1.重写所有 setter
set: function(target, key, value, receiver) {
throw err;
},
IsExtensible: function(target, receiver) {
return false;
},
deleteProperty: function(target, key, receiver) {
throw err;
},
defineProperty: function(target, key, desc, receiver) {
throw err;
},
setPrototypeOf: function(target, proto, receiver) {
throw err;
},
// 2.重写所有 getter
// getter 存在漏洞,x 只读,但 x.prop 仍然是可写的
// 篡改 x.prop 可能影响 x 内部逻辑
// 所以还要重写 get
get: function(target, key, receiver) {
var res = Reflect.get(target, key, receiver);
// if (typeof res === 'object') {
if (Object(res) === res) { // 判断 res 是不是 Object 类型
res = readOnly(res);
}
return res;
},
getPrototypeOf: this.get,
getOwnPropertyDescriptor: this.get
});
}
var openObj = {
attr: 1,
obj: {
val: 1,
getVal: function() {
return this.val;
}
},
fn: function() {
console.log('fn is called');
}
};
var closeObj = readOnly(openObj);
// 避免 throw err 中断执行流,影响测试
var nextTick = fn => setTimeout(fn, 50);
nextTick(() => console.log(closeObj.attr));
nextTick(() => closeObj.fn());
nextTick(() => closeObj.fn = console.log);
nextTick(() => delete closeObj.obj);
// 如果忘记限制 getter 就会出现 bug
nextTick(() => {
closeObj.obj.val = 'hack';
console.log(closeObj.obj.getVal()); // hack
})
readOnly の実装は想像以上に長く、1 つの改ざん防止オブジェクトにこれほど多くのことを考慮する必要があるのでしょうか?必要です。しかも任何一点を疎かにすると漏洞を引き起こします。プロキシ機制でデフォルト行為を書き換えるには非常に注意が必要です。上記の方案仍然存在問題:
// 访问 2 次,new2 个 proxy 对象
console.log(closeObj.obj === closeObj.obj); // false
proxy をキャッシュすることで不要なプロキシオブジェクトの作成を回避できます。Object をキャッシュする最も簡単な方法はもちろん [WeakMap](/articles/集合(set 和 map)-es6 笔记 8/) です。以下の通り:
function readOnlyEx(obj) {
var proxyCache = new WeakMap();
//...
if (Object(res) === res) { // 判断 res 是不是 Object 类型
// 缓存
if (!proxyCache.get(res)) {
proxyCache.set(res, readOnly(res));
}
res = proxyCache.get(res);
}
//...
}
// test
var _closeObj = readOnlyEx(openObj);
// 同一个 proxy 对象,因为有 WeakMap
console.log(_closeObj.obj === _closeObj.obj); // true
さらに、proxy !== target は注意が必要な問題です。例えば new Proxy(window, {}).alert(1) はエラーで実行できません。apply 処理が必要です。以下の通り:
// proxy !== target
// new Proxy(window, {}).alert(1);
// 会报错 TypeError: 'alert' called on an object that does not implement interface Window.
var mWin = new Proxy(window, {
get: function(target, key, receiver) {
var res = Reflect.get(target, key, receiver);
//! function 判断可能不够严密
if (typeof res === 'function') {
return function() {
return res.apply(target, arguments);
}
}
}
});
// test
mWin.alert(123); // alert 123
###3. 検証ロジックの分離
setter/getter の利点は、プロキシ機制を通じて検証ロジックをプロパティアクセス行為に内蔵できることです(検証ロジックを置く場所があります)
###4. オブジェクトアクセスの記録
主にテスト/デバッグで使用されます。例えばテストフレームワークで、プロキシ機制を通じてすべてを完全に記録できます
###5. 通常オブジェクトの強化
例えば自動填充オブジェクトと改ざん防止オブジェクトで、API 設計方面に大きな想像空間があります
###6.WeakMap との連携
同じプロキシの多次作成を回避します。例えば readOnlyEx で、WeakMap でプロキシオブジェクトを管理するのは非常に自然なことです
四.オブジェクトの内部メソッド
訳自:http://www.ecma-international.org/ecma-262/6.0/index.html#table-5
Table 5 — 必要な内部メソッド| 内部メソッド | シグネチャ | 説明 | 說明 |
|---|---|---|---|
| [[GetPrototypeOf]] | () → Object | Null | 現在のオブジェクトに継承属性を提供するオブジェクトを決定し、null値は継承された属性がないことを示します | obj.__proto__ または obj['__proto__'] または Object.getPrototypeOf(obj) を実行する時に呼び出されます |
| [[SetPrototypeOf]] | (Object | Null) → Boolean | 現在のオブジェクトと継承属性を提供する別のオブジェクトとの関連を確立し、nullを渡すと属性を継承しないことを示し、trueを返すと操作が正常に完了したことを示し、falseは操作失敗を示します | |
| [[IsExtensible]] | ( ) → Boolean | 現在のオブジェクトに追加の属性を追加することを許可するかどうかを決定します | |
| [[PreventExtensions]] | ( ) → Boolean | 新しい属性を現在のオブジェクトに追加できるかどうかを制御し、操作成功時はtrueを返し、否则falseを返します | |
| [[GetOwnProperty]] | (propertyKey) → Undefined | Property Descriptor | 現在のオブジェクトがpropertyKeyという名前の属性を所有している場合、このオブジェクトの独自プロパティに対応する属性記述子を返し、否则undefinedを返します | |
| [[HasProperty]] | (propertyKey) → Boolean | 現在のオブジェクトがすでにpropertyKeyという名前の属性を所有しているかどうか(該属性は現在のオブジェクトのものでも継承されたものでもよい)を示すブール値を返します | key in object を実行する時に呼び出されます |
| [[Get]] | (propertyKey, Receiver) → any | 現在のオブジェクトのpropertyKeyという名前の属性の値を返します。該属性値を(プロトタイプチェーンから)取得するために ES コードを実行する必要がある場合、Receiverをthis値として使用します | obj.attr または obj['attr'] を実行する時に呼び出されます |
| [[Set]] | (propertyKey,value, Receiver) → Boolean | propertyKeyという名前の属性値をvalueに設定します。該属性値を設定するために ES コードを実行する必要がある場合(属性はプロトタイプチェーンから)、Receiverをthis値として使用します。属性値設定成功時はtrueを返し、否则falseを返します | obj.attr/obj[attr] = val を実行する時に呼び出され、+=,++,--を実行する時は先に [[Get]] を呼び出しその後 [[Set]] を呼び出します |
| [[Delete]] | (propertyKey) → Boolean | 現在のオブジェクトからpropertyKeyという名前の属性を削除します。属性が削除されずに存在し続ける場合はfalseを返し、属性が削除されたか存在しない場合はtrueを返します | |
| [[DefineOwnProperty]] | (propertyKey, PropertyDescriptor) → Boolean | propertyKeyという名前の既存の属性を作成または修正し、PropertyDescriptorで記述された状態に設定します。属性が正常に作成/更新された場合はtrueを返し、否则falseを返します | |
| [[Enumerate]] | ()→Object | 現在のオブジェクトの列挙可能属性キーセットを生成できるイテレータオブジェクトを返します | for (var key in obj) を実行する時に呼び出され、内部メソッドは反復可能オブジェクトを返し、for...in ループはこの内部メソッドを通じてオブジェクトキーセットを取得できます |
| [[OwnPropertyKeys]] | ()→List of propertyKey | 現在のオブジェクトのすべての所有属性(プロトタイプチェーンからの属性を含まない)のキーで構成されるListを返します |
| 内部メソッド | シグネチャ | 説明 | 說明 |
|---|---|---|---|
| [[Call]] | (any, a List of any) → any | 現在のオブジェクトの関連コードを実行し、関数呼び出し式を通じて呼び出します。内部メソッドのパラメータはthis値と関数呼び出し式を通じて渡されたパラメータリストです。該内部メソッドを実装したオブジェクトはcallable(呼び出し可能)です。 | fn() または obj.fn() を実行する時に呼び出されます |
| [[Construct]] | (a List of any, Object) → Object | オブジェクトを作成し、new または super 演算子を通じて呼び出します。内部メソッドの最初のパラメータは演算子のパラメータリストで、2 番目のパラメータは最初にnew演算子が適用されたオブジェクトです。該内部メソッドを実装したオブジェクトはconstructors(コンストラクタ)と呼ばれ、関数オブジェクトは必ずしもコンストラクタではありません。このような非コンストラクタ関数オブジェクトには [[Construct]] 内部メソッドがありません | new Type() または super() を実行する時に呼び出されます |
P.S.super キーワードは親クラスコンストラクタ(super(args))を呼び出すか、親クラス属性にアクセスする(super.attr/super.fn(args)、class キーワードと組み合わせてクラスを定義します。詳細は super)ために使用されます
五.总结
js には組み込みのプロキシ機制があります。反射機制(非常に弱いですが)と組み合わせて温かく煎じて服用
注釈があるのでしょうか?このように見ると、js 自動テストロボットに目処が立ちました
参考資料
- 《ES6 in Depth》:InfoQ 中文站が提供する無料電子書籍
コメントはまだありません