proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
1.基本用法
let obj = {
name:"kafei",
arr:[1,2,3,4]
}
let proxy = new Proxy(obj,{
get(target,key,receiver){
// 相当于 return target[key]
return Reflect.get(target, key)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value, receiver)
}
})
proxy.name="zhangsan"
console.log(obj) // 张三
以上的代码为最基本的用法,主要知识点在于new Proxy接受两个参数,一个为目标元素,第二个参数为object,其中该object包含了set和get方法。
或许会产生了一下的疑问:
- 在set和get方法中都有receiver,该参数有何作用?
- 为什么使用Reflect?
1.1 Receiver
let proxy = new Proxy(obj,{
get(target,key,receiver){
console.log(receiver === proxy) // true
return Reflect.get(target, key)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value, receiver)
}
})
在get函数内部中我们可以看到 receiver === proxy 是为true的,那么在这个时候,就隐约感觉到它和this上下文有这关系。
let proxy = new Proxy(obj,{
get(target,key,receiver){
console.log(receiver === proxy)
console.log(this)
return Reflect.get(target, key)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value, receiver)
}
})
当我们在get函数中输出了this,此时的this是指向当前的对象(不是proxy对象,而是当前定义的set和get对象)
接下来在看个例子:
let obj = {
name:"kafei",
arr:[1,2,3,4],
get value(){
return "7777"
}
}
let proxy = new Proxy(obj,{
get(target,key,receiver){
// false
console.log(receiver === proxy)
// true
console.log(receiver === targetObj)
return Reflect.get(target, key)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value, receiver)
}
})
const targetObj = {
noKey: 'xiaohu',
};
Object.setPrototypeOf(targetObj, proxy);
targetObj.value
这个时候可以看到 receiver === traget的,由于.value是通过targetObj触发的,此时receiver指向的就是targetObj,那么也就是receiver存在的意义是为了传递正确的调用者指向,所以当我们涉及到属性访问的时候,需要注意到receiver指向问题。
1.1为什么使用Reflect?
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
简单来说,我们可以通过 Proxy 创建对于原始对象的代理对象,从而在代理对象中使用 Reflect 达到对于 JavaScript 原始操作的拦截。,其主要的用法与js中Math类似。
那么以上的demo能否不使用Reflect呢?我个人认为是可以的。直接 return target[key],它的效果是一致的。
但是面对于例子2则无能为力了。上述的例子中当我们输出targetObj.value时候返回的是’kafei’
let obj = {
name:"kafei",
arr:[1,2,3,4],
get value(){
return this.name
}
}
const targetObj = {
name: 'xiaohu',
};
let proxy = new Proxy(obj,{
get(target,key){
return Reflect.get(target, key);
},
set(target, key, value, receiver){
return Reflect.set(target, key, value)
}
})
Object.setPrototypeOf(targetObj, proxy);
console.log(targetObj.value) // kafei
这个并不是我预期的结果,如果我希望返回的是xiaohu,那么这个时候receiver则发挥了作用
let obj = {
name:"kafei",
arr:[1,2,3,4],
get value(){
return this.name
}
}
const targetObj = {
name: 'xiaohu',
};
let proxy = new Proxy(obj,{
get(target,key,receiver){
return Reflect.get(target, key,receiver);
},
set(target, key, value, receiver){
return Reflect.set(target, key, value)
}
})
Object.setPrototypeOf(targetObj, proxy);
console.log(targetObj.value) // xiaohu
可以简单理解 Reflect.get(target, key, receiver) === target[key].call(receiver)
2.proxy特殊情况
2.1 代理函数
上述例子提到,new Proxy第一个参数是object,那么我第一个参数是否能够是个函数呢?
根据mdn的介绍中,
扩展构造函数
方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。可以使用apply。
由于涉及到函数,那么我们重点来看下,它的this(上下文)指向问题
const foo = (a, b, c) => {
return a + b + c;
}
const pFoo = new Proxy(foo, {
apply: (target, that, args) => {
const grow = args.map(x => x * 2);
// undefined
console.log(that)
const inter = Reflect.apply(target, that, grow);
return inter * 3;
}
});
pFoo(1, 2, 3); // 36, (1 * 2 + 2 * 2 + 3 * 2) * 3
细心的朋友可能会注意到apply函数入参,入了that,此时我打印可发现that是undefined的,这是由于我们没有对函数本身进行this绑定
let obj = {
foo:(res)=>{ // 自定义的业务逻辑
return res*5
}
}
const foo = (a, b, c) => {
return a + b + c; // 原本的业务逻辑
}
const pFoo = new Proxy(foo, {
apply: (target, that, args) => {
const inter = Reflect.apply(target, that, args);
console.log(inter,"inter") // 原本的结果 7
return that.foo(inter);
}
});
console.log(pFoo.call(obj,2,2,3));// 35
可以通过proxy代理出某个函数的返回,然后再进行我们的自定义操作,这种场景适合在,你不需要关心函数的具体实现逻辑,只需要关心它的返回值是不是你想要的,如果不是你想要的话,可通过proxy进行二次修改。
2.2 监听数组变化
熟悉vue的朋友都知道在vue2.6+版本是通过Object.defineproperty()来监听对象发生变化的,在vue3.0版本则全面使用proxy。但由于Object.defineproperty()特性导致,无法监听到数组的变化,则需要通过’绕’的方式来进行操作。
伪代码如下:
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原来的方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
通过重写数组的原有方法,主要思路如下:
- 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
- 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作
- 把需要被拦截的 Array 类型的数据原型指向改造后原型。
那么再使用proxy则无需考虑这点。
const raw = []
const arr = new Proxy(raw, {
get(target, key) {
console.log('get', key)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key)
return Reflect.set(target, key, value)
}
})
arr.push(1)
需要注意的就是 get和set都会重复触发,所以需要添加个判断。
3.proxy场景
处理嵌套对象获取值
在业务处理中,我们需要处理到某个对象上面的a.b.c的情况,可以通过proxy来进行获取,例如给定对象:
{
a:{
b:{
c:1
}
}
}
console.log(a.b.c) // 1
function getValueByPath(object, path, defaultValue) {
let proxy = new Proxy(object, {
get(target, key) {
if (key.startsWith('.')) {
key = key.slice(1);
}
if (key.includes('.')) {
path = path.split('.');
let index = 0, len = path.length;
while(target != null && index < len) {
target = target[path[index++]]
}
return target || defaultValue;
}
if (!(key in target) || !target[key]) {
return defaultValue
}
return Reflect.get(target, key)
}
});
return proxy[path]
}
其实这种方式在loadsh库中_.get()方法也能实现,但是我们也可以通过链表的方式来实现该功能。
const getValueByPath = function(object, prop) {
prop = prop || '';
const paths = prop.split('.');
let current = object;
let result = null;
let len = paths.length
let i = 0
while(i<len){
let path = paths[i]
if(!current){
break
}
if(i === len - 1){
result = current[path]
break
}
current = current[path]
i++
}
return result;
};