-
概述
Proxy 用于修改某些操作的默認(rèn)行為钧排,可以理解成灯谣,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn)曲掰,都必須先通過(guò)這層攔截疾捍,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫蜈缤。
ES6 原生提供 Proxy 構(gòu)造函數(shù)拾氓,用來(lái)生成 Proxy 實(shí)例。
var proxy = new Proxy(target, handler);
Proxy 對(duì)象的所有用法底哥,都是上面這種形式咙鞍,不同的只是handler參數(shù)的寫法房官。其中,new Proxy()表示生成一個(gè)Proxy實(shí)例续滋,target參數(shù)表示所要攔截的目標(biāo)對(duì)象翰守,handler參數(shù)也是一個(gè)對(duì)象,用來(lái)定制攔截行為疲酌。
-
Proxy 實(shí)例的方法
get()
get方法用于攔截某個(gè)屬性的讀取操作蜡峰,可以接受三個(gè)參數(shù),依次為目標(biāo)對(duì)象朗恳、屬性名和 proxy 實(shí)例本身(嚴(yán)格地說(shuō)湿颅,是操作行為所針對(duì)的對(duì)象),其中最后一個(gè)參數(shù)可選粥诫。
var person={name:'小明'};
var proxyPerson=new Proxy(person,{
get:function(target,property){
if(property in target){
return target[property]}
else{
throw new ReferenceError("Property \"" + property + "\" does not exist.")
}
}
})
proxy.name // "小明"
proxy.age //Uncaught ReferenceError: Property "age" does not exist.
上面代碼表示油航,如果訪問(wèn)目標(biāo)對(duì)象不存在的屬性,會(huì)拋出一個(gè)錯(cuò)誤怀浆。如果沒(méi)有這個(gè)攔截函數(shù)谊囚,訪問(wèn)不存在的屬性,只會(huì)返回undefined执赡。
get方法可以繼承镰踏。
var child=Object.create(proxyPerson);
child.name //小明
上面代碼中,攔截操作定義在Prototype對(duì)象上面沙合,所以如果讀取obj對(duì)象繼承的屬性時(shí)奠伪,攔截會(huì)生效。
set()
set方法用來(lái)攔截某個(gè)屬性的賦值操作灌诅,可以接受四個(gè)參數(shù)芳来,依次為目標(biāo)對(duì)象、屬性名猜拾、屬性值和 Proxy 實(shí)例本身即舌,其中最后一個(gè)參數(shù)可選。
let personq=new Proxy({},{
set:function(obj,prop,value){
if(prop==='age'){if(!Number.isInteger(value)){
throw new TypeError('The age is not an integer')}
if(value>200){
throw new RangeError('The age seems invalid')}
}
obj[prop]=value
}})
上面代碼中挎袜,由于設(shè)置了存值函數(shù)set顽聂,任何不符合要求的age屬性賦值,都會(huì)拋出一個(gè)錯(cuò)誤盯仪,這是數(shù)據(jù)驗(yàn)證的一種實(shí)現(xiàn)方法紊搪。利用set方法,還可以數(shù)據(jù)綁定全景,即每當(dāng)對(duì)象發(fā)生變化時(shí)耀石,會(huì)自動(dòng)更新 DOM。
apply()
apply方法攔截函數(shù)的調(diào)用爸黄、call和apply操作滞伟。
apply方法可以接受三個(gè)參數(shù)揭鳞,分別是目標(biāo)對(duì)象、目標(biāo)對(duì)象的上下文對(duì)象(this)和目標(biāo)對(duì)象的參數(shù)數(shù)組梆奈。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
has()
has方法用來(lái)攔截HasProperty操作野崇,即判斷對(duì)象是否具有某個(gè)屬性時(shí),這個(gè)方法會(huì)生效亩钟。典型的操作就是in運(yùn)算符乓梨。
has方法可以接受兩個(gè)參數(shù),分別是目標(biāo)對(duì)象清酥、需查詢的屬性名扶镀。
下面的例子使用has方法隱藏某些屬性,不被in運(yùn)算符發(fā)現(xiàn)焰轻。
var animal=new Proxy(target,{
has(target,key){
if(key[0]==='_'){
return false}
return target[key]
}
})
上面代碼中狈惫,如果原對(duì)象的屬性名的第一個(gè)字符是下劃線,proxy.has就會(huì)返回false鹦马,從而不會(huì)被in運(yùn)算符發(fā)現(xiàn)。
另外忆肾,雖然for...in循環(huán)也用到了in運(yùn)算符荸频,但是has攔截對(duì)for...in循環(huán)不生效。
deleteProperty()
deleteProperty方法用于攔截delete操作客冈,如果這個(gè)方法拋出錯(cuò)誤或者返回false旭从,當(dāng)前屬性就無(wú)法被delete命令刪除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
上面代碼中场仲,deleteProperty方法攔截了delete操作符和悦,刪除第一個(gè)字符為下劃線的屬性會(huì)報(bào)錯(cuò)。
注意渠缕,目標(biāo)對(duì)象自身的不可配置(configurable)的屬性鸽素,不能被deleteProperty方法刪除,否則報(bào)錯(cuò)亦鳞。
其他實(shí)例方法就不在一一贅述馍忽,可以參考https://es6.ruanyifeng.com/#docs/proxy
-
使用場(chǎng)景
proxy 模式一般可以使用在當(dāng)你想要:
攔截或者控制對(duì)一個(gè)對(duì)象的訪問(wèn)時(shí)
通過(guò)掩蓋程序或隱藏邏輯來(lái)降低方法/類的復(fù)雜度
阻止沒(méi)有經(jīng)過(guò)驗(yàn)證/準(zhǔn)備的重資源操作
1. 抽離校驗(yàn)代碼
function createValidator(target, validator) {
return new Proxy(target, {
_validator: validator,
set(target, key, value, proxy) {
if (target.hasOwnProperty(key)) {
let validator = this._validator[key];
if (!!validator(value)) {
return Reflect.set(target, key, value, proxy);
} else {
throw Error(`Cannot set ${key} to ${value}. Invalid.`);
}
} else {
// prevent setting a property that isn't explicitly defined in the validator
throw Error(`${key} is not a valid property`)
}
}
});
}
// Now, just define validators for each property
const personValidators = {
name(val) {
return typeof val === 'string';
},
age(val) {
return typeof age === 'number' && age > 18;
}
}
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
return createValidator(this, personValidators);
}
}
const bill = new Person('Bill', 25);
這樣,你就能在不改變你的類/方法的前提下無(wú)線擴(kuò)展你的校驗(yàn)代碼了燕差。
2. JavaScript 中真正的私有
通過(guò)get(),set()實(shí)例方法可以保證對(duì)象里面私有屬性真正實(shí)現(xiàn)私有遭笋。
var api = {
_apiKey: '123abc456def',
/* mock methods that use this._apiKey */
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
// Add other restricted properties to this array
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} is restricted. Please see api documentation for further info.`);
}
return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} is restricted. Please see api documentation for further info.`);
}
return Reflect.get(target, key, value, proxy);
}
});
// throws an error
console.log(api._apiKey);
// throws an error
api._apiKey = '987654321';
3. 靜默對(duì)象訪問(wèn)日志
對(duì)于資源密集、運(yùn)行緩慢或重度使用的方法和接口徒探,你也許想要記錄他們的使用情況或性能瓦呼。Proxies 可以很容易的在后臺(tái)默默的做這件事。
每當(dāng)你調(diào)用一個(gè)方法测暗,你都會(huì)先 get 這個(gè)方法央串。所以如果你想攔截一個(gè)方法調(diào)用磨澡,你需要攔截它的 get 先,然后再攔截 apply蹋辅。
let api = {
_apiKey: '123abc456def',
getUsers: function() { /* ... */ },
getUser: function(userId) { /* ... */ },
setUser: function(userId, config) { /* ... */ }
};
api = new Proxy(api, {
get: function(target, key, proxy) {
var value = target[key];
return function(...arguments) {
logMethodAsync(new Date(), key);
return Reflect.apply(value, target, arguments);
};
}
});
// executes apply trap in the background
api.getUsers();
function logMethodAsync(timestamp, method) {
setTimeout(function() {
console.log(`${timestamp} - Logging ${method} request asynchronously.`);
}, 0)
}
- 警告或阻止特定操作
設(shè)你想要組織某人刪除 noDelete 屬性钱贯,想要告訴調(diào)用 oldMethod 的用戶那已經(jīng)被棄用了,或者想組織某人修改 doNotChange 屬性
let dataStore = {
noDelete: 1235,
oldMethod: function() {/*...*/ },
doNotChange: "tried and true"
};
const NODELETE = ['noDelete'];
const DEPRECATED = ['oldMethod'];
const NOCHANGE = ['doNotChange'];
dataStore = new Proxy(dataStore, {
set(target, key, value, proxy) {
if (NOCHANGE.includes(key)) {
throw Error(`Error! ${key} is immutable.`);
}
return Reflect.set(target, key, value, proxy);
},
deleteProperty(target, key) {
if (NODELETE.includes(key)) {
throw Error(`Error! ${key} cannot be deleted.`);
}
return Reflect.deleteProperty(target, key);
},
get(target, key, proxy) {
if (DEPRECATED.includes(key)) {
console.warn(`Warning! ${key} is deprecated.`);
}
var val = target[key];
return typeof val === 'function' ?
function(...args) {
Reflect.apply(target[key], target, args);
} :
val;
}
});
// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";
delete dataStore.noDelete;
dataStore.oldMethod();
- 組織不必要的重資源操作
假設(shè)你有個(gè)會(huì)返回一個(gè)很大的文件的服務(wù)端侦另,你不希望在之前的請(qǐng)求正在進(jìn)行時(shí)秩命,或者文件正在被下載時(shí),或者已經(jīng)下載完畢的情況下進(jìn)行請(qǐng)求褒傅。 - 即時(shí)取消對(duì)敏感數(shù)據(jù)的訪問(wèn)
Proxy 提供任何時(shí)候取消對(duì)目標(biāo)對(duì)象訪問(wèn)功能弃锐。如果你想要完全封鎖(為了安全性、權(quán)限殿托、或性能原因)一些數(shù)據(jù)和 API 的訪問(wèn)這會(huì)很有用霹菊。