Proxy
Proxy用于创建一个对象的代理,从而修改某些操作的默认行为。可以理解成在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,让我们可以对外界的访问进行过滤和改写,这些过滤,可以由我们自己来定义:
语法
cosnt p = new Proxy(target, handler);
参数
target
要使用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler
是一个以函数作为属性的对象,其属性是当执行各种操作时代理的函数(可以理解为对象某些操作的捕捉器)。
// 定义一个和张三一样普通的对象
let userInfo = {
name: "kobe"
};
// 我们用proxy做一个简单的代理
userInfo = new Proxy(userInfo, {
// 设置一个读取和操作的捕捉器
get(target, prop) {
console.log('getter:获取了对象属性' + prop);
return target[prop]
},
set(target, prop, value) {
console.log(`setter: 操作对象属性${prop},值为${value}`);
// 也可以写一些属性设置的条件'l
if(prop === 'age' && value >= 200) {
throw new RangeError("这怕不是个老妖怪吧");
}
// 符合条件的属性保存
target[prop] = value;
return true;
}
});
// 兄弟们让我们来操作它
// 读取userInfo的属性看会发生什么
console.log(userInfo.name); // getter:获取了对象属性name kobe
// 给userInfo添加新的属性age
userInfo.age = 41; // setter: 操作对象属性age,值为41 41
// 添加另外属性
userInfo.gender = 'male'; // setter: 操作对象属性gender,值为male 'male'
++userInfo.age;
// getter:获取了对象属性age
// setter: 操作对象属性age,值为42
从上面的例子中,我们先定义了一个包含name
的用户信息对象userInfo
,然后我们通过Proxy
包装,再返回给userInfo
,此时的userInfo
就成了一个 Proxy
实例,我们对其进行的操作,都会被 Proxy
拦截。对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
handler 对象的方法
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, property, receiver): 拦截对象属性的读取。比如
userInfo.name
。 - set(target, property, value, receiver): 拦截对象属性的设置,比如
userInfo.age = 18
, 返回一个布尔值。 - has(target, property): 拦截
prop in proxy
的操作,返回一个布尔值。 - deleteProperty(target, property): 拦截
delete
对象属性的操作,返回一个布尔值。 - ownKeys(target): 拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, property): 拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, property, 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, prototype): 拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args): 拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。可以返回任何值. - construct(target, args): 拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
handler.get()
var userInfo = {
name: "张三"
}
const user = new Proxy(userInfo, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
throw new ReferenceError("prop name \"" + prop + "\" does not exist.");
}
}
})
user.name // "张三"
user.age // Uncaught ReferenceError: prop name "age" does not exist.
handler.set()
let handler = {
set(target, prop, value) {
if(prop === "age") {
if(!Number.isInteger(value))
if(!Number.isInteger(value))
throw new TypeError('The age is not an integer');
if(value > 200)
throw new RangeError('这怕不是个老妖怪吧!')
}
target[prop] = value;
return true;
}
}
let user = new Proxy({}, handler);
user.name = "张三"; // '张三'
user.age = 100; // 100
user.age = 1.1; // Uncaught TypeError: The age is not an integer
user.age = "old"; // Uncaught TypeError: The age is not an integer
user.age = 250; // Uncaught RangeError: 这怕不是个老妖怪吧!
handler.has()
has()
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
has()
方法可以接受两个参数,分别是目标对象、需查询的属性名。
下面的例子使用has()方法隐藏某些属性,不被in
运算符发现。
let handler = {
has(target, prop) {
if(prop.indexOf('_') > -1) {
return false;
}
return true;
}
}
let person = {
_name: "zhang",
name: "张三",
old_name: "张狗蛋"
}
let user = new Proxy(person, handler);
"name" in user; // true
"_name" in user; // false
"old_name" in user; // false
handler.deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
// 定义狗蛋信息对象
let person = {
name: "狗蛋",
ID_number: "88888888",
age: 25,
gender: 'male'
}
// 创建代理对象
let user = new Proxy(person, {
deleteProperty(target, prop) {
// 输出信息
console.log(`老子要删除你的${prop}`);
if(prop === 'name' || prop === 'ID_number') {
throw new Error(`二货,爸爸的${prop}是你能删的吗!!!`)
}
return true;
}
})
delete user.age // true
delete user.name // Uncaught Error: 二货,爸爸的name是你能删的吗!!!
handler.ownKeys()
ownKeys()
方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in循环
// 1、拦截Object.keys()的例子
let person = {
name: "狗蛋",
ID_number: "88888888",
age: 25,
gender: 'male'
}
let user = new Proxy(person, {
ownKeys(target) {
// 返回属性中不包含下划线的
return Object.keys(target).filter(key => key.indexOf('_') < 0)
}
})
Object.keys(user); // ['name', 'age', 'gender']
// 2、拦截Object.getOwnPropertyNames()
Object.getOwnPropertyNames(user); // ['name', 'age', 'gender']
// 3、for...in循环
for (let key in user) {
console.log(key); // name age gender
}
// 返回自己没有的属性
let user = new Proxy(person, {
ownKeys(target) {
return ['name', 'age', 'a', 'b']
}
})
for (let key in user) {
console.log(key); // name age
}
ownKeys()
方法返回的数组成员,只能是字符串或Symbol
值。如果有其他类型的值,或者返回的根本不是数组,就会报错
handler.getOwnPropertyDescriptor()
getOwnPropertyDescriptor()
方法拦截Object.getOwnPropertyDescriptor()
,返回一个属性描述对象或者undefined
。
const user = new Proxy(person, {
getOwnPropertyDescriptor(target, prop) {
if(prop === "ID_number") return;
return Object.getOwnPropertyDescriptor(target, prop);
}
})
Object.getOwnPropertyDescriptor(user, 'name');
// {configurable: true, enumerable: true, value: "狗蛋", writable: true}
Object.getOwnPropertyDescriptor(user, 'ID_number');
// undefined
handler.defineProperty()
defineProperty()
方法拦截了Object.defineProperty()
操作。
const user = new Proxy(person, {
defineProperty (target, prop, desc) {
return false;
}
})
user.foo = "bar"; // 不会生效
defineProperty()方法内部没有任何操作,导致添加新属性总是无效。
注意: 如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty()方法不得改变这两个设置。
handler.preventExtensions()
preventExtensions()
方法拦截Object.preventExtensions()
。该方法必须返回一个布尔值,否则会被自动转为布尔值。
这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)
为false
),proxy.preventExtensions
才能返回true
,否则会报错。
var user = new Proxy(person, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(user);
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
上面代码中,proxy.preventExtensions()
方法返回true
,但这时Object.isExtensible(proxy)
会返回true,因此报错。
为了防止出现这个问题,通常要在proxy.preventExtensions()
方法里面,调用一次Object.preventExtensions()
。
var user = new Proxy(person, {
preventExtensions: function(target) {
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(user); // Proxy {...}
handler.getPrototypeOf()
getPrototypeOf()
方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
- -
Object.prototype.__proto__
- -
Object.prototype.isPrototypeOf()
- -
Object.getPrototypeOf()
- -
Reflect.getPrototypeOf()
- -
instanceof
// 定义一个和你一样普通的对象
var obj = {};
var user = new Proxy({}, {
getPrototypeOf(target) {
return obj;
}
})
Object.getPrototypeOf(user) === obj // true
getPrototypeOf()
方法拦截Object.getPrototypeOf()
,返回obj
对象。
注意,getPrototypeOf()
方法的返回值必须是对象或者null
,否则报错。另外,如果目标对象不可扩展,getPrototypeOf()
方法必须返回目标对象的原型对象。
handler.setPrototypeOf()
setPrototypeOf()
方法主要用来拦截Object.setPrototypeOf()
方法。
var obj = {};
var target = function () {};
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var user = new Proxy(target, handler);
Object.setPrototypeOf(user, obj); // Error: Changing the prototype is forbidden
上面代码中,只要修改target
的原型对象,就会报错。
注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展,setPrototypeOf()
方法不得改变目标对象的原型。
handler.isExtensible()
isExtensible()
方法拦截Object.isExtensible()
操作。Object.isExtensible()
方法用于判断一个对象是否是可扩展的。
var user = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(user); // called true
注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。
handler.apply()
apply
方法拦截函数的调用、call
和apply
操作。
apply
方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this
)和目标对象的参数数组。
var person = function() {
return "我是个比张三还普通的人";
}
var handler = {
apply() {
return "我特么的被拦截了!"
}
}
var user = new Proxy(person, handler);
user(); // '我特么的被拦截了!'
user.call(); // '我特么的被拦截了!'
user.apply(); // '我特么的被拦截了!'
Reflect.apply(user, null, []); // '我特么的被拦截了!'
上面代码中,每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截。
另外,直接调用Reflect.apply方法,也会被拦截。
handler.construct()
construct()
方法用于拦截new
命令,下面是拦截对象的写法。
var user = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { name: args[0]};
}
});
new user('张三');
// called: 张三
// {name: '张三'}
由于construct()
拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。
注意,construct()方法中的this指向的是handler,而不是实例对象。
实例
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。
const service = createWebService('http://exam.com/data');
service.employees().then(json => {
// ....
})
新建一个web服务的接口,Proxy可以拦截这个对象的任意属性,所以不用为每一种数据写适配方法,只要用proxy拦截就可以了。
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, prop) {
return Http.get(`${baseUrl}/{prop}`)
},
// ...
});
}