1 关于Proxy
Proxy,代理,是ES6推出的一个特性。通过Proxy我们可以拦截对象的某些操作,并在其中加入定制化代码,使这些操作变得更加丰富灵活
语法:
let proxy = new Proxy(target,handle)
Proxy表示创建代理实例的类,target为被代理的对象,handle为拦截器对象,内含拦截器方法
2 Proxy常用API
- set 拦截属性写入
- get 拦截属性读取
- deleteProerty 拦截属性删除
- has 拦截in操作符
- revocable 撤销proxy
...
2.1 Proxy api示例: set
Proxy有许多方法,这里挑一个最具代表性的set方法作为示例
在Proxy Handle中,set用于拦截对target对象的属性赋值操作
set拥有四个参数,分别是被代理对象target,属性名key,属性值value以及set方法的调用者receiver(类比于this一般指向调用者obj),这个receiver一般是proxy,但也有例外,后续会说明
在代理之后,一旦通过proxy为对象属性赋值,proxy中的set(如果有)就会执行。反之则不会,即直接为对象赋值并不会触发Proxy set方法
let person = {name:'island'}
let proxy = new Proxy(person,{
set(target,key,value,receiver){
console.log('set run')
return Reflect.set(target,key,value,receiver)
}
})
proxy.name = 'proxy set' // set run
person.name = 'person set' // nothing happen
target一定是被代理对象。而receiver一般是代理对象proxy,但其实是指向set方法的调用者
let person = {name:'island'}
let proxy = new Proxy(person,{
set(target,key,value,receiver){
console.log(target === person) // true
console.log(receiver === proxy ) // true
}
})
proxy.name = 'person set'
如果将proxy写入到person的原型上,此时给person设置属性时,js会从person自身向原型链上找到set并执行,此时set的调用者是person而非peoxy,相应的receiver也变成了person,因此我们说receiver是set方法的调用者而并非一定是proxy
let person = {};
let proxy = new Proxy({}, {
set(target, prop, value, receiver) {
console.log(receiver === person) // true
console.log(receiver === proxy) // false
}
});
Object.setPrototypeOf(person, proxy);
person.name = 'island'
注意:
- set方法只能对target的可写属性(writable为true)作用,writable为false时set失效
- set方法中不建议对receiver再进行属性赋值,因为这会重复触发set执行,抛出函数执行栈溢出异常。同理,我们也不建议在get方法中对receiver做属性读取操作
let person = {name:'isand'};
let proxy = new Proxy(person, {
set: function (target, prop, value, receiver) {
receiver[prop] = value // Maximum call stack size exceeded
},
get(target,key,receiver){
receiver[key] // Maximum call stack size exceeded
}
});
proxy.name = 'yoke'
3 Proxy的应用
Proxy以其自身丰富灵活的特点,可以在许多开发场景中得到应用
在工程中,前端是UI与后台数据的桥梁,我们经常遇见的一个场景是将接口数据中的时间戳进行格式化展现给用户,或者对表单的输入进行合理的校验,而这些都可以通过Proxy代理来完成
4 Proxy与被代理对象的关系
无论是通过Proxy改变被代理对象target的属性,还是直接改变被操作对象target的属性,在Proxy对象与target中都能得到反馈。关于Proxy与被代理对象target究竟是什么关系,本人也一直很疑惑。有人倾向于认为是浅拷贝,但我认为不是,如果是浅拷贝,那么改变了非引用类型的属性时,是不会作用到对方内存中的。但事实显然与之相悖。无论是改变proxy.string还是target.string,都能在对方身上看到效果,所以二者并不是浅拷贝的关系。
我暂且觉得Proxy与target之间是有能力访问同一块内存的关系,但又并非是赋值引用,因为proxy === target 值为假。这个问题暂且放着,可能要等到日后阅读V8实现,或者社区有人阅读了V8相关实现并对外分享之后才能得到解释了
5 Proxy与Object.defineProperty的差异
Object.defineProperty中也有get,set方法,这与Proxy有一部分共性。Vue2.0中使用了Object.defineProperty来进行数据劫持,实现数据双向绑定。但Vue3.0中尤大又用Proxy重写了这一特性,从而不得不引发大家关于Proxy与Object.defineProperty之间差异性的讨论
从语义上来说,defineProperty专注于属性被操作时的权限,比如属性是否可修改--writable,是否可配置--configurable。而Proxy更在意属性被操作时的行为,比如对象属性被遍历之前时应当做些什么--has,被删除之前应该做些什么--deleteProerty
从细节上来说,Proxy对属性监听的更深入一些。定义一个属性值为数组,对于Proxy,数组内部元素的改变也会被侦听到,触发set trap。而defineProperty的set只在属性指向地址更改的时候才执行,而数组内部元素的改变并不会被侦听到,正是由于defineProperty的这一特征,导致Vue2.0需要重写数组原型上的push、splice等方法
let arrProxy = new Proxy([], {
set(target, key, value, receiver) {
console.log('run Proxy set')
}
})
let target = {}
Object.defineProperty(target,'arr',{
set:function(){
console.log('run defineProperty set ')
}
})
arrProxy.push(1) // run Proxy set
target.arr.push(1) // nth happpen