目录
一、概述
Proxy用于修改某些“操作”的“默认行为”。等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供一种机制,可以对外界的访问进行过滤和改写。
一个简单的例子:
const obj = new Proxy({},{
get(target,prop,receiver){
console.log(`获得${prop}`);
return Reflect.get(target,prop,receiver);
},
set(target,prop,value,receiver){
console.log(`设置${prop}=${value}`);
return Reflect.set(target,prop,value,receiver);
}
})
obj.count = 1; // 设置count=1
++obj.count; // //获得count,设置count=2
1.1Proxy构造函数
ES6原生提供Proxy构造函数,用来生成Proxy实例:
const proxy = new Proxy(target,handler);
- new Proxy():生成一个Proxy实例
- target:所拦截的目标对象
- handler:一个对象,用来定制拦截行为
1.2拦截读取属性行为的例子
const proxy = new Proxy({},{
get(target,prop){
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.age // 35
上面代码中,Proxy接收两个参数。第一个参数是要代理的对象(一个空对象);第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数拦截对应的操作。
由于上面代码使用get方法拦截对目标属性的访问请求,因此访问任何属性都得到35。
1.3注意点
要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对“目标”进行操作。
如果“handler”没有设置任何拦截,则就等同于直接通向原对象。
const target = {};
const handler = {};
const proxy = new Proxy(target,handler);
proxy.a = 1;
console.log(target.a); // 1
Proxy实例也可以作为其它对象的原型对象
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。
1.4拦截操作
同一个拦截器函数(handler)可以设置多个操作。
对于:“可以设置、但没有设置拦截的操作”,则直接落在目标对象上,按照原先的方式产生结果。
Proxy支持的拦截操作一共13种,如下所示:
- get(target,propKey,receiver):拦截对象属性的读取(proxy.foo或proxy['foo'])
- set(target,propKey,value,receiver):拦截对象属性的设置(proxy.foo = v或proxy['foo'] = v),返回一个布尔值
- has(target,propKey):拦截propKey in prop的操作,返回一个布尔值
- deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
- ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in
- getOwnPropertyDescriptor(target,propKey):拦截Object.getOwnProperty(proxy,propKey,propDesc),返回属性的描述对象
- getdefineProperty(target,propKey,propDesc):拦截Object.defineProperty(proxy,propKey,propDesc)、Object.defineProperties(proxy,propDescs),返回一个布尔值
- preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
- getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
- isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
- setPrototypeOf(target,proto):拦截Object.setPrototypeOf(proxy,proto),返回一个布尔值
- apply(target,thisArg,args):拦截Proxy实例作为函数调用的操作(proxy(..args)、proxy.call(object,...args)、proxy.apply(...))
- construct(target,args):拦截Proxy实例作为构造函数调用的操作(new proxy(...args))
【解释】一些参数解释:
- target:目标对象(目标函数),即被代理的对象(函数)
- propKey:属性名
- receiver:代理对象本身或继承自代理对象的对象。通常情况下,没有特殊情况,receiver就是代理对象
- value:要设置的属性值
- property:属性名
- thisArg:函数调用时的this值
- args:函数调用时传递的参数列表(数组形式)
二、proxy的方法
在这里只介绍常用方法
2.1get()
get(target,propKey,receiver)方法接受三个参数,依次为:“目标对象”、“属性名”、“proxy实例本身”,最后一个参数可选
另一个拦截读取操作的例子:
const proxy = new Proxy(person,{
get(target,prop,receiver){
if (prop in target){
return target[prop];
}
else {
throw new ReferenceError(`${prop}未找到`);
}
}
});
console.log(proxy.name); // 张三
console.log(proxy.age); // ReferenceError: age未找到
在上面代码中,如果访问目标对象不存在的属性,会抛出一个操作。如果没有这个拦截函数,访问不存在的属性,只会undefined。
function createArray(...elements){
console.log(elements); // [1, 2, 3]
const handler = {
get(target,prop,receiver){
const index = Number(prop);
if (index < 0){
prop = String(target.length + index);
}
return Reflect.get(target,prop,receiver);
}
}
const target = [];
target.push(...elements);
return new Proxy(target,handler);
}
const arr = createArray(1,2,3);
console.log(arr[-1]); // 3
上面代码中,如果数组的位置参数为负数,就会从数组右侧开始计算
2.2set()
set(target,prop,value,receiver)方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为:“目标对象”、“属性名”、“属性值”和Proxy实例,最后一个参数可选
现在我们假设Person对象有一个age属性,该属性应该是一个不大于100的整数,那么可以使用Proxy保证属性值符合要求
const handler = {
set(target,prop,value){
if (prop === 'age'){
if (!Number.isInteger(value)){
throw new TypeError('age必须是整数');
}
if (value > 100){
throw new RangeError('age不能大于100');
}
}
// 对于满足条件的age属性和其他属性,直接赋值
obj[prop] = value;
}
}
const person = new Proxy({},handler);
person.age = '人'; // 抛出TypeError: age必须是整数
person.age = 120; // 抛出RangeError: age不能大于100
上面代码中,使用了set()拦截方法,任何不符合要求的set属性负值,都会抛出一个错误,这是数据验证的一种实现方法。
2.3has()
has(target,prop)方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是“in”运算符
has方法可以接受两个参数,分别是:“目标对象”、“查询的属性名”
下面的例子使用has方法隐藏某些属性,不被in运算符发现
const handler = {
has(target,prop){
if (prop[0] === '_'){
return false;
}
return key in target;
}
};
const target = {
_count: 1,
count:1
};
const proxy = new Proxy(target,handler);
console.log('_count' in proxy); // false
has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。
另外,虽然for...in循环也用到了in运算符,但是has拦截对for...in循环不生效
2.4ownKeys()
ownKeys(target)方法用来拦截对象自身属性的读取操作,具体来说,拦截以下操作:
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- for...in循环
const target = {
a:1,
b:2,
c:3
};
const handler = {
ownKeys(target){
return ['a'];
}
};
const proxy = new Proxy(target,handler);
console.log(Object.keys(proxy)); // ['a']
注意:使用Object.keys()方法时,有三雷属性会被ownKeys()方法自动过滤:
- 目标对象上不存在的属性
- 属性名为Symbol值
- 不可遍历(enumerable)的属性
for...in循环也受到ownKeys()方法的拦截:
const obj = { hello: 'world' };
const proxy = new Proxy(obj, {
ownKeys: function () {
return ['a', 'b'];
}
});
for (let key in proxy) {
console.log(key); // 没有任何输出
}
ownKeys()只返回a和b属性,由于obj没有这两个属性,因此for...in循环不会有任何输出