详细探究ES6之Proxy代理
前言
在ES6中,Proxy构造器是一种可访问的全局对象,使用它你可以在对象与各种操作对象的行为之间收集有关请求操作的各种信息,并返回任何你想做的。ES6中的箭头函数、数组解构、rest参数等特性一经实现就广为流传,但类似Proxy这样的特性却很少见到有开发者在使用,一方面在于浏览器的兼容性,另一方面也在于要想发挥这些特性的优势需要开发者深入地理解其使用场景。就我个人而言是非常喜欢ES6的Proxy,因为它让我们以简洁易懂的方式控制了外部对对象的访问。在下文中,首先我会介绍Proxy的使用方式,然后列举具体实例解释Proxy的使用场景。
Proxy,见名知意,其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:
1.和监视外部对对象的访问
2.函数或类的复杂度
3.操作前对操作进行校验或对所需资源进行管理
在支持Proxy的浏览器环境中,Proxy是一个全局对象,可以直接使用。Proxy(target,handler)是一个构造函数,target是被代理的对象,handlder是声明了各类代理操作的对象,最终返回一个代理对象。外界每次通过代理对象访问target对象的属性时,就会经过handler对象,从这个流程来看,代理对象很类似middleware(中间件)。那么Proxy可以拦截什么操作呢?最常见的就是get(读取)、set(修改)对象属性等操作,完整的可拦截操作列表请点击这里。此外,Proxy对象还提供了一个revoke方法,可以随时注销所有的代理操作。在我们正式介绍Proxy之前,建议你对Reflect有一定的了解,它也是一个ES6新增的全局对象。
Basic
consttarget={
name:'BillyBob',
age:15
};
consthandler={
get(target,key,proxy){
consttoday=newDate();
console.log(`GETrequestmadefor${key}at${today}`);
returnReflect.get(target,key,proxy);
}
};
constproxy=newProxy(target,handler);
proxy.name;
//=>"GETrequestmadefornameatThuJul21201615:26:20GMT+0800(CST)"
//=>"BillyBob"
在上面的代码中,我们首先定义了一个被代理的目标对象target,然后声明了包含所有代理操作的handler对象,接下来使用Proxy(target,handler)创建代理对象proxy,此后所有使用proxy对target属性的访问都会经过handler的处理。
1.抽离校验模块
让我们从一个简单的类型校验开始做起,这个示例演示了如何使用Proxy保障数据类型的准确性:
letnumericDataStore={
count:0,
amount:1234,
total:14
};
numericDataStore=newProxy(numericDataStore,{
set(target,key,value,proxy){
if(typeofvalue!=='number'){
throwError("PropertiesinnumericDataStorecanonlybenumbers");
}
returnReflect.set(target,key,value,proxy);
}
});
//抛出错误,因为"foo"不是数值
numericDataStore.count="foo";
//赋值成功
numericDataStore.count=333;
如果要直接为对象的所有属性开发一个校验器可能很快就会让代码结构变得臃肿,使用Proxy则可以将校验器从核心逻辑分离出来自成一体:
functioncreateValidator(target,validator){
returnnewProxy(target,{
_validator:validator,
set(target,key,value,proxy){
if(target.hasOwnProperty(key)){
letvalidator=this._validator[key];
if(!!validator(value)){
returnReflect.set(target,key,value,proxy);
}else{
throwError(`Cannotset${key}to${value}.Invalid.`);
}
}else{
throwError(`${key}isnotavalidproperty`)
}
}
});
}
constpersonValidators={
name(val){
returntypeofval==='string';
},
age(val){
returntypeofage==='number'&&age>18;
}
}
classPerson{
constructor(name,age){
this.name=name;
this.age=age;
returncreateValidator(this,personValidators);
}
}
constbill=newPerson('Bill',25);
//以下操作都会报错
bill.name=0;
bill.age='Bill';
bill.age=15;
通过校验器和主逻辑的分离,你可以无限扩展personValidators校验器的内容,而不会对相关的类或函数造成直接破坏。更复杂一点,我们还可以使用Proxy模拟类型检查,检查函数是否接收了类型和数量都正确的参数:
letobj={
pickyMethodOne:function(obj,str,num){/*...*/},
pickyMethodTwo:function(num,obj){/*...*/}
};
constargTypes={
pickyMethodOne:["object","string","number"],
pickyMethodTwo:["number","object"]
};
obj=newProxy(obj,{
get:function(target,key,proxy){
varvalue=target[key];
returnfunction(...args){
varcheckArgs=argChecker(key,args,argTypes[key]);
returnReflect.apply(value,target,args);
};
}
});
functionargChecker(name,args,checkers){
for(varidx=0;idx<args.length;idx++){
vararg=args[idx];
vartype=checkers[idx];
if(!arg||typeofarg!==type){
console.warn(`Youareincorrectlyimplementingthesignatureof${name}.Checkparam${idx+1}`);
}
}
}
obj.pickyMethodOne();
//>YouareincorrectlyimplementingthesignatureofpickyMethodOne.Checkparam1
//>YouareincorrectlyimplementingthesignatureofpickyMethodOne.Checkparam2
//>YouareincorrectlyimplementingthesignatureofpickyMethodOne.Checkparam3
obj.pickyMethodTwo("wopdopadoo",{});
//>YouareincorrectlyimplementingthesignatureofpickyMethodTwo.Checkparam1
//Nowarningslogged
obj.pickyMethodOne({},"alittlestring",123);
obj.pickyMethodOne(123,{});
2.私有属性
在JavaScript或其他语言中,大家会约定俗成地在变量名之前添加下划线_来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。在下面的代码中,我们声明了一个私有的apiKey,便于api这个对象内部的方法调用,但不希望从外部也能够访问api._apiKey:
varapi={
_apiKey:'123abc456def',
/*mockmethodsthatusethis._apiKey*/
getUsers:function(){},
getUser:function(userId){},
setUser:function(userId,config){}
};
//logs'123abc456def';
console.log("AnapiKeywewanttokeepprivate",api._apiKey);
//getandmutate_apiKeysasdesired
varapiKey=api._apiKey;
api._apiKey='987654321';
很显然,约定俗成是没有束缚力的。使用ES6Proxy我们就可以实现真实的私有变量了,下面针对不同的读取方式演示两个不同的私有化方法。
第一种方法是使用set/get拦截读写请求并返回undefined:
letapi={
_apiKey:'123abc456def',
getUsers:function(){},
getUser:function(userId){},
setUser:function(userId,config){}
};
constRESTRICTED=['_apiKey'];
api=newProxy(api,{
get(target,key,proxy){
if(RESTRICTED.indexOf(key)>-1){
throwError(`${key}isrestricted.Pleaseseeapidocumentationforfurtherinfo.`);
}
returnReflect.get(target,key,proxy);
},
set(target,key,value,proxy){
if(RESTRICTED.indexOf(key)>-1){
throwError(`${key}isrestricted.Pleaseseeapidocumentationforfurtherinfo.`);
}
returnReflect.get(target,key,value,proxy);
}
});
//以下操作都会抛出错误
console.log(api._apiKey);
api._apiKey='987654321';
第二种方法是使用has拦截in操作:
varapi={
_apiKey:'123abc456def',
getUsers:function(){},
getUser:function(userId){},
setUser:function(userId,config){}
};
constRESTRICTED=['_apiKey'];
api=newProxy(api,{
has(target,key){
return(RESTRICTED.indexOf(key)>-1)?
false:
Reflect.has(target,key);
}
});
//theselogfalse,and`forin`iteratorswillignore_apiKey
console.log("_apiKey"inapi);
for(varkeyinapi){
if(api.hasOwnProperty(key)&&key==="_apiKey"){
console.log("Thiswillneverbeloggedbecausetheproxyobscures_apiKey...")
}
}
3.访问日志
对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用Proxy充当中间件的角色,轻而易举实现日志功能:
letapi={
_apiKey:'123abc456def',
getUsers:function(){/*...*/},
getUser:function(userId){/*...*/},
setUser:function(userId,config){/*...*/}
};
functionlogMethodAsync(timestamp,method){
setTimeout(function(){
console.log(`${timestamp}-Logging${method}requestasynchronously.`);
},0)
}
api=newProxy(api,{
get:function(target,key,proxy){
varvalue=target[key];
returnfunction(...arguments){
logMethodAsync(newDate(),key);
returnReflect.apply(value,target,arguments);
};
}
});
api.getUsers();
4.预警和拦截
假设你不想让其他开发者删除noDelete属性,还想让调用oldMethod的开发者了解到这个方法已经被废弃了,或者告诉开发者不要修改doNotChange属性,那么就可以使用Proxy来实现:
letdataStore={
noDelete:1235,
oldMethod:function(){/*...*/},
doNotChange:"triedandtrue"
};
constNODELETE=['noDelete'];
constNOCHANGE=['doNotChange'];
constDEPRECATED=['oldMethod'];
dataStore=newProxy(dataStore,{
set(target,key,value,proxy){
if(NOCHANGE.includes(key)){
throwError(`Error!${key}isimmutable.`);
}
returnReflect.set(target,key,value,proxy);
},
deleteProperty(target,key){
if(NODELETE.includes(key)){
throwError(`Error!${key}cannotbedeleted.`);
}
returnReflect.deleteProperty(target,key);
},
get(target,key,proxy){
if(DEPRECATED.includes(key)){
console.warn(`Warning!${key}isdeprecated.`);
}
varval=target[key];
returntypeofval==='function'?
function(...args){
Reflect.apply(target[key],target,args);
}:
val;
}
});
//thesewillthrowerrorsorlogwarnings,respectively
dataStore.doNotChange="foo";
deletedataStore.noDelete;
dataStore.oldMethod();
5.过滤操作
某些操作会非常占用资源,比如传输大文件,这个时候如果文件已经在分块发送了,就不需要在对新的请求作出相应(非绝对),这个时候就可以使用Proxy对当请求进行特征检测,并根据特征过滤出哪些是不需要响应的,哪些是需要响应的。下面的代码简单演示了过滤特征的方式,并不是完整代码,相信大家会理解其中的妙处:
letobj={
getGiantFile:function(fileId){/*...*/}
};
obj=newProxy(obj,{
get(target,key,proxy){
returnfunction(...args){
constid=args[0];
letisEnroute=checkEnroute(id);
letisDownloading=checkStatus(id);
letcached=getCached(id);
if(isEnroute||isDownloading){
returnfalse;
}
if(cached){
returncached;
}
returnReflect.apply(target[key],target,args);
}
}
});
6.中断代理
Proxy支持随时取消对target的代理,这一操作常用于完全封闭对数据或接口的访问。在下面的示例中,我们使用了Proxy.revocable方法创建了可撤销代理的代理对象:
letsensitiveData={username:'devbryce'};
const{sensitiveData,revokeAccess}=Proxy.revocable(sensitiveData,handler);
functionhandleSuspectedHack(){
revokeAccess();
}
//logs'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
//TypeError:Revoked
console.log(sensitiveData.username);
Decorator
ES7中实现的Decorator,相当于设计模式中的装饰器模式。如果简单地区分Proxy和Decorator的使用场景,可以概括为:Proxy的核心作用是控制外界对被代理者内部的访问,Decorator的核心作用是增强被装饰者的功能。只要在它们核心的使用场景上做好区别,那么像是访问日志这样的功能,虽然本文使用了Proxy实现,但也可以使用Decorator实现,开发者可以根据项目的需求、团队的规范、自己的偏好自由选择。
总结
ES6的Proxy还是非常实用的,看似简单的特性,却有极大的用处。希望给大家学习ES6有所帮助。