proxy详细介绍与使用
proxy
对象用于创建一个对象的代理,是在目标对象之前架设一个拦截,外界对该对象的访问,都必须先通过这个拦截。通过这种机制,就可以对外界的访问进行过滤和改写。
ES6
原生提供 Proxy
构造函数,用来生成 Proxy
实例。
var proxy = new Proxy(target, handler);
target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
const obj = {};
const proxy=new Proxy(obj,{
get:function(target,propKey){
return 1
}
})
console.log(proxy.name) // 1
console.log(proxy.value) // 1
在上面代码中,Proxy
接受两个参数,第一个是要代理的目标对象(上面的是空对象obj
),在没有设置Proxy
的情况下,对proxy
的操作就是访问obj
。第二个参数是配置对象,对每个被代理的操作,需要提供一个对应的处理函数,这个函数用于拦截对应的操作。
上面代码中,配置对象有个get
方法,用来拦截对目标对象属性的访问请求。get
方法中的两个参数分别是目标对象和所要访问的属性。由于设置的是返回1,所以访问任何属性都得到1。
如果handler
没有设置拦截,就等同于直接通向原对象。
const obj = {};
const proxy=new Proxy(obj, {})
proxy.a = 1
console.log(obj.a) // 1
console.log(obj === proxy) // false
handler
是空对象,没有拦截作用,访问proxy
等同于访问obj
。但是代理对象和原对象并不相等。
handler
配置
handler
对象是一个容纳一批特定属性的占位符对象。它包含有 proxy
的各个捕获器。
1. get
get
方法用于拦截某个属性的读取操作,接受三个参数,(目标对象,属性名,Proxy
或者proxy
实例本身),第三个参数可选。
const obj={
value:1
}
const proxy=new Proxy(obj,{
get:function(target,propKey){
if(propKey in target){
return target[propKey]
}else{
throw new ReferenceError(`${propKey} 不存在`)
}
}
})
console.log(proxy.value) // 1
proxy.name //报错
如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined
。
get
方法可以被继承
const obj = {
a: 1
};
let proto = new Proxy(obj, {
get(target, propertyKey) {
return target[propertyKey];
}
});
let newObj = Object.create(proto);
newObj.a // "GET foo"
拦截操作定义在原型对象上面,所以如果读取obj
对象继承的属性时,拦截会生效。
下面是一个get
方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy
实例。
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true
对于
get
方法,proxy
存在一些约束
- 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同,否则会抛出异常。
const obj = {}; Object.defineProperty(obj, "a", { configurable: false, enumerable: false, value: 1, writable: false }); const proxy=new Proxy(obj,{ get:function(target,propKey){ return 2 // return 1 就没问题 } }); proxy.a
- 如果要访问的目标属性没有配置访问方法,即
get
方法是undefined
的,则返回值必须为undefined
。const obj = {}; Object.defineProperty(obj, "a", { configurable: false, get: undefined }); const proxy=new Proxy(obj,{ get:function(target,propKey){ return 1; // return undefined 就没问题 } }); proxy.a
2. set
用来拦截某个属性的赋值操作,接受四个参数(目标对象、属性名、属性值、最初被调用的对象(通常是proxy
实例本身,但 handler
的 set
方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy
本身))),第四个参数可选。
const proxy = new Proxy({}, {
set: function(obj,prop,value){
if(prop === 'value') {
if(!Number.isInteger(value)){
throw new TypeError('不是整数')
}
if(value>10){
throw new RangeError('太大了')
}
}
obj[prop] = value
}
})
proxy.value = 5
proxy.value = 'val' //报错
proxy.value = 20 //报错
由于设置了存值函数set
,任何不符合要求的value
属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。
来看看set
在原型链上的情况.
const proxy = new Proxy({}, {
set: function(obj, prop, value, receiver) {
console.log(receiver === newObj); // true
if(prop === 'value') {
if(!Number.isInteger(value)){
throw new TypeError('不是整数')
}
if(value>10){
throw new RangeError('太大了')
}
}
obj[prop] = value
}
})
let newObj = Object.create(proxy);
newObj.value = 1;
将代理对象作为原型创建一个新对象,此时第四个参数就是这个新对象而不是proxy
实例。
对于
set
方法,proxy
也存在一些约束
- 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
const obj = {}; Object.defineProperty(obj, "a", { configurable: false, enumerable: false, value: 1, writable: false }); const proxy=new Proxy(obj,{ set: function(obj, prop, value) { obj[prop] = value } }); proxy.a = 1; proxy.a // 还是 undefined
- 如果目标属性没有配置存储方法,即
set
的是undefined
,则不能设置它的值.const obj = {}; Object.defineProperty(obj, "a", { configurable: false, set: undefined }); const proxy=new Proxy(obj,{ set: function(obj, prop, value) { obj[prop] = value } }); proxy.a = 1; proxy.a // 还是 undefined
- 在严格模式下,如果
set
方法返回false
,那么也会抛出一个异常。
3. apply
拦截函数的调用,接受三个参数,分别是目标对象、目标对象的上下文对象(this
)和目标对象的参数数组。
var target = function () { return 'target'; };
var handler = {
apply: function () {
return 'proxy';
}
};
var p = new Proxy(target, handler);
p()
// "proxy"
变量p
是 Proxy
的实例,当它作为函数调用时(p()
),就会被apply
方法拦截,返回一个字符串。
4. has
用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。接受两个参数,分别是目标对象、需查询的属性名。
var handler = {
has (target, key) {
return key in target;
}
};
var target = { prop: 'foo' };
var proxy = new Proxy(target, handler);
'propa' in proxy // false
如果原对象不可配置或者禁止扩展,这时has
拦截会报错。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
has
方法拦截的是HasProperty
操作,而不是HasOwnProperty
操作,即has
方法不判断一个属性是对象自身的属性,还是继承的属性。可以看作是针对 in
操作的钩子。
5. construct
用于拦截new
命令
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
接受三个参数,目标对象,构造函数的参数对象,new
命令作用的构造函数。
construct
方法返回的必须是一个对象,否则会报错。
6. deletePrototype
拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被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
操作符,删除第一个字符为下划线的属性会报错。
目标对象自身的不可配置(configurable
)的属性,不能被deleteProperty
方法删除,否则报错。
7. defineProperty
拦截了Object.defineProperty
操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var proxy = new Proxy({}, handler);
proxy.a = 'a' // 不会生效
8. getOwnPropertyDescriptor
拦截Object.getOwnPropertyDescriptor()
,返回一个属性描述对象或者undefined
。
var handler = {
getOwnPropertyDescriptor (target, key) {
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = {a: 'a' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'a')
// { value: 'a', writable: true, enumerable: true, configurable: true }
9. getPrototypeOf
用来拦截获取对象原型。
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
10. isExtensible
拦截Object.isExtensible
操作。
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
11. ownKeys
拦截对象自身属性的读取操作。
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]
12. preventExtensions
拦截Object.preventExtensions()
。
var proxy = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
13. setPrototypeOf
用来拦截Object.setPrototypeOf
方法。
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
只要修改target
的原型对象,就会报错。
proxy
比Object.defineProperty
具有的优势
了解Proxy
的基本用法过后接下来我们再深入探讨一下相比于Object.defineProperty
, Proxy
到底有哪些优势。
首先最明显的优势就是在于Proxy
要更为强大一些,那这个强大具体体现在Object.defineProperty
只能监听到对象属性的读取或者是写入,而Proxy
除读写外还可以监听对象中属性的删除,对对象当中方法的调用等等。
这里我们为obj
对象定义一个Proxy
对象,在Proxy
对象的处理对象中的外的添加一个deleteProperty
的代理方法,这个方法会在外部对当前这个代理对象进行delete
操作时会自动执行。
这个方法同样接收两个参数,分别是代理目标对象和所要删除的这个属性的名称。
const obj = {
a: 1
}
const proxy = new Proxy(obj, {
deleteProperty(target, propKey) {
delete target[propKey]
}
})
delete proxy.a
vue2.x
的响应式并不具备这样的处理,而是需要一个$delete
方法来解决。
export default {
data() {
return {
obj: {
a: 1
}
}
},
methods: {
deleteA() {
delete this.obj.a; // a属性确实删除了,但是没有触发对应的更新
// this.$delete(this.obj, 'a') 能删除a,并触发更新
}
}
}
比如上方的使用delete
直接删除a
,并不会触发更新,这是因为Object.defineProperty
只能监听到对象属性的读取或者是写入,没办法监听删除。我们顺便来看看vue2.x
的解决办法:
function del(target: any[] | object, key: any) {
// ...
if (isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target as any).__ob__
// ...
delete target[key]
if (!ob) {
return
}
// 获取对象的观察者__ob__,并手动调用更新
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.DELETE,
target: target,
key
})
} else {
ob.dep.notify()
}
}
可以看到 vue
实例的 $delete
处理了一些边界情况,处理了数组和对象的删除,最重要的是,获取了变量上的ob
指针指向的观察者实例,并通知依赖dep
里的watcher
更新。
第二点优势就是对于数组对象进行监视,
通常我们想要监视数组的变化,基本要依靠重写数组方法,这也是vue2.x
的实现方式,proxy
可以直接监视数组的变化。以往我们想要通过Object.defineProperty
去监视数组的操作最常见的方式是重写数组的操作方法,大体的方式就是通过自定义的方法去覆盖掉数组原型对象上的push
,shift
之类的方法,以此来劫持对应的方法调用的过程。
这里来看如何直接使用Proxy
对象来对数组进行监视。这里我们定义一个list
数组,然后对这个list
数组进行Proxy
监视。
在这个Proxy
对象的处理对象上我们去添加一个set
方法,用于监视数据的写入,在这个方法的内部我们打印参数的值,然后再target
对象上设置传入的值,最后返回一个true
表示写入成功。
这样我们再外部对数组的写入都会被监视到,例如我们这里通过push
向数组中添加值。
const arr = []
const proxy = new Proxy(arr, {
set(target, propKey, value) {
target[propkey] = value;
return true
}
})
Proxy
内部会自动根据push
操作推断出来他所处的下标,每次添加或者设置都会定位到对应的下标property
。
最后相比于Object.defineProperty
还有一点优势就是,Proxy
是以非入侵的方式监管了对象的读写,那也就是说一个已经定义好的对象我们不需要对对象本身去做任何的操作,就可以监视到他内部成员的读写,而defineProperty
的方式就要求我们必须按特定的方式单独去定义对象当中那些被监视的属性。