Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy可以理解为,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截 ,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy的原意是代理,用在这里表示它来”代理“某些操作,可以译为”代理器“。
var obj=new Proxy({},{
get:function(target,propKey,receiver){
console.log(`getting ${propKey}!`);
return Reflect.get(target,propKey,receiver);
},
set:function(target,propKey,value,receiver){
console.log(`setting ${propKey}!`);
return Reflect.set(target,propKey,value,receiver);
}
})
上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写他的属性,就会得到下面的结果。
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代码说明,Proxy实际上重载了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6原生提供了Proxy构造函数,用来生成Proxy实例。
var proxy=new Proxy(target,handler);
Proxy对象的所有用法,都是上面这种形式,不同的是handler参数的写法。其中,new Proxy()表示生成一个proxy 实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
下面是另一个拦截读取属性行为的例子。
var proxy=new Proxy({},{
get:function(target,propKey){
return 35;
}
});
proxy.time //35
proxy.name //35
proxy.title //35
上面的代码中,作为构造函数,Proxy接收两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。
注意:要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。
一个技巧就是将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };
Proxy实例也可以作为其他对象的原型对象。
var proxy=new Proxy({},{
get:function(target,propKey){
return 35;
}
})
let obj=Object.create(proxy);
obj.time //35
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链 ,会在proxy对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
get方法的用法,上文已经有一个实例,下面是一个拦截读取操作的例子。
var person={
name:"张三"
};
var proxy=new Proxy(person,{
get:function(target,propKey){
if(propKey in target){
return target[propKey];
}else{
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.")
}
}
});
proxy.name //“张三”
proxy.age //“抛出一个错误”
上面的代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有拦截,访问不存在的属性,只会返回undefined。
get方法可以继承。
let proto=new Proxy({},{
get(target,propertyKey,receiver){
console.log('GET'+propertyKey);
return target[propertyKey];
}
})
let obj=Object.create(proto);
obj.foo//"GET foo"
上面的代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。
假定Person对象有一个age属性,该属性应该是一个不大于200的整数,那么可以使用proxy保证age的属性值符合要求。
let validator={
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;
return true;
}
};
let person=new Proxy({},validator);
person.age //100
person.age='young' //报错
person.age=300 //报错
上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用set方法,还可以数据绑定,即每当对象发生改变的时候,会自动更新DOM。
有时,我们会在对象上设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get和set方法,就可以做到防止这些内部属性被外部读写。
const handler={
get(target,key){
invariant(key,'get');
return target[key];
},
set(target,key){
invariant(key,'set');
target[key]=value;
return true;
}
};
function invariant(key,action){
if(key[0]==='_'){
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target={};
const proxy=new Proxy(target,handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上面的代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
var handler={
apply(target,ctx,args){
return Reflect.apply(...arguments);
}
}
下面是一个例子。
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;
上面的代码中,变量p是Proxy的实例,当他作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。