引言
大家都知道Vue3和Vue2之间很核心的改变就是使用Proxy代理来替换Object.defineProperty()。之前文章也提到了为什么Object.defineProperty不能监听到数组长度的变化?感兴趣的可以看下。
我们可以利用 Proxy 来实现对于对象的代理劫持操作。但是你也许并不清楚为什么要在Proxy里使用Reflect。
本文将通过几个例子来阐述它们之间的关系(假设你已经了解了什么是Proxy & Reflect),以及最后会讲述为什么必须要使用Reflect。
如何使用Proxy
const obj = {
name: 'jiapandong',
};
const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key) {
console.log('劫持你的数据访问' + key);
return target[key]
},
});
proxy.name // 劫持你的数据访问name -> jiapandong
我们通过 Proxy 创建了一个基于 obj 对象的代理,同时在 Proxy 中声明了一个 get 陷阱。
当访问我们访问 proxy.name 时实际触发了对应的 get 陷阱,它会执行 get 陷阱中的逻辑,同时会执行对应陷阱中的逻辑,最终返回对应的 target[key]
也就是jiapandong
。
Proxy 中的 receiver
在查看MDN文档的时候会发现,get中还存在一个额外的参数receiver
。那么它到底表示什么意思呢?有文章会把它理解为代理对象
。
这么理解是否正确呢?接下来,我们来举个简单的例子
const obj = {
name: 'jiapandong',
};
const proxy = new Proxy(obj, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});
// 打印:true jiapandong
proxy.name;
上述的例子中,我们在 Proxy 实例对象的 get 陷阱上接收了 receiver 这个参数。
同时,我们在陷阱内部打印 console.log(receiver === proxy)
; 它会打印出 true
,表示这里 receiver 的确是和代理对象相等的。
所以 receiver 的确是可以表示代理对象。
再来,我们看另一个例子
const parent = {
get value() {
return 'wuyanzu';
},
};
const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
console.log(receiver === proxy);
return target[key];
},
});
const obj = {
name: 'jiapandong',
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
// 打印: false
obj.value
我们可以看到,上述的代码同样我在 proxy 对象的 get 陷阱上打印了console.log(receiver === proxy)
; 结果却是 false
。
其实这就是 proxy 中 get 陷阱第三个 receiver 存在的意义:为了传递正确的调用者指向
。下面我们验证一下:
...
const proxy = new Proxy(parent, {
// get陷阱中target表示原对象 key表示访问的属性名
get(target, key, receiver) {
- console.log(receiver === proxy) // 打印: false
+ console.log(receiver === obj) // 打印: true
return target[key];
},
});
...
其实简单来说,get 陷阱中的 receiver 存在的意义就是为了正确的在陷阱中传递上下文。
我们可以清楚的看到上述的 receiver 代表的是继承与 Proxy 的对象,也就是 obj。
如上所得一个结论:Proxy 中 get 陷阱的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时它会代表继承 Proxy 的那个对象。
本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问,比如这里的 receiver 指向的是 obj 。
Reflect 中的 receiver
在清楚了 Proxy 中 get的 receiver 后,我们来聊聊 Reflect 反射 API 中 get 陷阱的 receiver。
const parent = {
name: 'wuyanzu',
get value() {
return this.name;
},
};
const handler = {
get(target, key, receiver) {
return Reflect.get(target, key);
// 这里相当于 return target[key]
},
};
const proxy = new Proxy(parent, handler);
const obj = {
name: 'jiapandong',
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
console.log(obj.value); // wuyanzu
我们分析下上面的代码:
- 当我们调用 obj.value 时,由于 obj 本身不存在 value 属性。
- 它继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。
- 此时会触发 proxy 上的 get value() 属性访问操作。
- 同时由于访问了 proxy 上的 value 属性访问器,所以此时会触发 get 陷阱。
- 进入陷阱时,target 为源对象也就是 parent ,key 为 value 。
- 陷阱中返回
Reflect.get(target,key)
相当于target[key]
。 - 此时,不知不觉中 this 指向在 get 陷阱中被偷偷修改掉了!!
- 原本调用方的 obj 在陷阱中被修改成为了对应的 target 也就是 parent 。
- 自然而然打印出了对应的
parent[value]
也就是 wuyanzu 。
这显然不是我们期望的结果,当我访问 obj.value 时,我希望应该正确输出对应的自身上的 name 属性也就是所谓的 obj.value => jiapandong
。
那么,Relfect 中 get 陷阱的 receiver 就展现出它的作用了。
const parent = {
name: 'wuyanzu',
get value() {
return this.name;
},
};
const handler = {
get(target, key, receiver) {
- return Reflect.get(target, key);
+ return Reflect.get(target, key, receiver);
},
};
const proxy = new Proxy(parent, handler);
const obj = {
name: 'jiapandong',
};
// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);
console.log(obj.value); // jiapandong
上述代码原理其实非常简单:
- 首先,之前我们提到过在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。
- 其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。
看到这里你应该已经明白 Relfect 中的 receiver 代表的含义是什么了:它可以修改属性访问中的 this 指向为传入的 receiver 对象
。
小结
Proxy因为要保证正确的this上下文指向,所以需要配合Reflect使用。
- Proxy 中接受的 Receiver 形参表示代理对象本身或者继承与代理对象的对象。
- Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。
最后:那么为什么必须要使用Reflect?
有细心的小伙伴看到这会产生一个想法:那既然是为了改变this的指向,为什么不可以直接使用target[key].call(receiver)
呢?
1. Reflect和Proxy配对使用,提供对象语义的默认行为。
有如下代码:
const proxy = new Proxy(obj, {
set(...args) {
console.log('set', ...args);
return Reflect.set(...args;);
},
});
我们也可以写成:
set(target, key, value) {
console.log('set', target, key, value);
target[key] = value;
},
然而,想get/set这样的方法,我们比较容易知道其语义所对应的语法,但有些方法比如ownKeys
,如果不用Reflect.ownkeys
,就必须要自己去实现。Proxy上所有的方法在Reflect上都有一对一的实现。
最近发现Reflect.set有严重的性能问题,其实严格来说,这不能说Reflect有严重的性能问题,而是使用了反射,就失去了jit所能进行的许多优化(所谓fast path)。这在所有语言里都是类似的,只要用了反射,性能一定是差的。
2. 将 Object 对象一些内部的方法,放到 Reflect 对象上。比如 Object.defineProperty
现阶段这些方法存在于 object 和 Reflect 对象上,未来只存在于 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
3. 操作对象出现报错返回 false
比如:Object.defineProperty(obj,name,desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回 false,这样会更合理一些。
// 旧写法
try {
Object.defineProperty(target, property, attributes);
} catch (err) {
//failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
//success
} else {
//failure
}
4. 让操作对象的编程变为函数式编程
老写法有的是命令式编程,比如下面这个例子
// 老写法
"assign" in Object; // true
// 新写法
Reflect.has(Object, "assign"); //true