第一次看 Vue 3 的响应式源码,很多人会卡在同一个地方:get trap 里明明写 return target[key] 就够了,为什么源码非要写成 Reflect.get(target, key, receiver)?多传一个参数到底有什么用?
这个问题的答案,正好就是 Reflect 存在的全部理由。它不是给 Object 换了个马甲,而是给「对象上的基础操作」提供了一套和 Proxy trap 严格一一对应的函数入口,让你在拦截之后能把默认行为原样补回去,还不丢失 this 指向。这篇把 Reflect 的四条设计理由讲清楚,逐个过一遍它的 API,重点落在 receiver 参数上。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- Reflect 出现的四条设计理由,每条解决什么实际问题
- 报错改返回布尔值,对代码结构的影响
- 操作符变函数带来的可组合性
Reflect.set的receiver参数为什么会触发defineProperty拦截- 13 个 Reflect 方法逐个过一遍,附对应的旧写法
- 什么时候该用 Reflect,什么时候 Object 就够了
# 一、简介
# 1.1 什么是 Reflect
Reflect 是为操作对象而提供的一套新 API。
它有两个和别的内置对象不一样的地方,值得先说清楚。第一,Reflect 不是构造函数,不能 new,它就是一个普通对象,上面挂了 13 个静态方法。第二,它的每一个方法都和 Proxy 的 trap 一一对应,名字和参数完全对得上。这个「一一对应」不是巧合,是设计出来的。
# 1.2 为什么要设计 Reflect
第一,将 Object 对象的属于语言内部的方法放到 Reflect 对象上。
Object 这个构造函数身上现在挂了一堆东西,有些是给日常业务用的(Object.assign、Object.keys),有些其实是语言内部的元操作(Object.defineProperty、Object.getOwnPropertyDescriptor)。这两类东西混在一起,职责很不清楚。Reflect 的第一个作用就是给后一类找个归宿,以后新增的语言内部方法只往 Reflect 上加,Object 保持干净。
第二,将用老 Object 方法报错的情况,改为返回 false。
// 旧写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
这个改动看着小,实际影响不小。try...catch 是为「异常」准备的控制流,而「这个属性不可配置所以定义失败」根本算不上异常,它是一个很正常的、可预期的结果。用布尔返回值表达可预期的失败,用异常表达真正的意外,代码读起来清楚多了。
第三,让 Object 操作变成函数行为。
// 旧写法
'name' in Object // true
// 新写法
Reflect.has(Object, 'name') // true
in、delete 这些是操作符,不是函数,没法传给别的函数当参数,也没法放进数组里遍历。变成函数之后就能组合了:
const keys = ['a', 'b', 'c'];
const missing = keys.filter(k => !Reflect.has(obj, k));
// 用操作符写不出这么直白的一行
第四,Reflect 与 Proxy 是相辅相成的,在 Proxy 上有的方法,在 Reflect 上就一定有。
let target = {}
let handler = {
set(target, proName, proValue, receiver){
// 确认对象的属性赋值成功
let isSuccess = Reflect.set(target, proName, proValue, receiver)
if(isSuccess){
console.log("成功")
}
return isSuccess
}
}
let proxy = new Proxy(target, handler)
这条是四条里最重要的。你拦截了一个操作,做完自己的事情之后,总得把默认行为执行掉,不然代理对象就废了。而「默认行为」是什么?就是 Reflect 上同名的那个方法。参数列表和 trap 完全一致,返回值也刚好是 trap 要求的类型,抄过去就能用。
确保对象的属性能正确赋值,广义上讲,即确保对象的原生行为能够正常进行,这就是 Reflect 的作用。
不用 Reflect 行不行?简单场景确实可以写 target[key] = value,但有两个问题。一是它没有返回值,你没法告诉 trap 赋值到底成没成功,而严格模式下 set trap 必须返回 true,你只能硬编码一个 return true,撒谎。二是丢了 receiver,原型链上的 getter/setter 里 this 会指错,这个下面 2.3 节展开。
# 二、Reflect 的 API
这 13 个方法和 Proxy 的 trap 一一对应,名字都能对上,学一遍等于学两个东西。
# 2.1 Reflect.get(target, property, receiver)
查找并返回 target 对象的 property 属性。
let obj = {
name: "poetries",
}
let result = Reflect.get(obj, "name")
console.log(result) // poetries
第三个参数 receiver 在有 getter 的时候才会显出作用:
let obj = {
// 属性 yu 部署了 getter 读取函数
get yu(){
// this 返回的是 Reflect.get 的 receiver 参数对象
return this.name + this.age
}
}
let receiver = {
name: "shen",
age: "18",
}
let result = Reflect.get(obj, "yu", receiver)
console.log(result) // shen18
yu 这个 getter 定义在 obj 上,但执行的时候 this 被换成了 receiver,所以拿到的是 shen 和 18,而不是 obj 上的(obj 上压根没有 name 和 age,不传 receiver 的话结果是 undefinedundefined)。
一句话概括:receiver 决定了 getter/setter 执行时 this 指向谁。
注意:如果 Reflect.get() 的第一个参数不是对象,会抛 TypeError。
# 2.2 Reflect.set(target, propName, propValue, receiver)
设置 target 对象的 propName 属性为 propValue,返回一个布尔值表示是否成功。
let obj = {
name: "poetries"
}
let result = Reflect.set(obj, "name", "静观流叶")
console.log(result) // true
console.log(obj.name) // 静观流叶
# 2.3 Reflect.set 与 Proxy.set 的配合
Reflect.set 与 Proxy 的 set 联合使用,并且传入 receiver,会额外触发一次定义属性的操作。
let obj = {
name: "chen"
}
let handler = {
set(target, key, value, receiver){
console.log("Proxy 拦截赋值操作")
// Reflect 完成赋值操作
return Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute){
console.log("Proxy 拦截定义属性操作")
// Reflect 完成定义属性操作
return Reflect.defineProperty(target, key, attribute)
}
}
let proxy = new Proxy(obj, handler)
proxy.name = "ya"
// Proxy 拦截赋值操作
// Proxy 拦截定义属性操作
原文这两个 trap 都没有 return,严格模式下会直接抛错,上面补上了。
那为什么 Reflect.set() 传入 receiver 参数,就会触发定义属性的操作?
原文的解释是「因为 Proxy.set() 中的 receiver 是 Proxy 的实例,即 obj」,这句有个事实错误,receiver 是 proxy 对象本身,不是 obj,两者是不同的对象。
正确的链路是这样的。set trap 收到的第四个参数 receiver 就是那个 proxy 对象。你把它原样传给 Reflect.set,Reflect.set 的规则是「把属性赋到 receiver 身上」,而 receiver 是 proxy,于是这次赋值又走了一遍代理。赋值到底怎么落地?内部会调 [[DefineOwnProperty]],也就是触发 defineProperty trap。所以你看到了两条日志。
这个行为不是 bug,但确实容易写出无限递归。如果你在 defineProperty trap 里又往 receiver 上写,那就转不出来了。上面这段之所以安全,是因为 Reflect.defineProperty 的第一个参数是 target 而不是 proxy,落到了原对象上,链条终止。
那到底该不该传 receiver?我的判断是:涉及原型链继承和 getter/setter 的场景必须传,否则 this 会指错,Vue 3 的 reactive 就是这么做的;纯粹给自己对象读写、没有继承关系的简单场景,不传也不会出问题。Proxy 那边完整的 trap 清单和实战用法我写在 ES6系列之 Proxy 的拦截机制与实战场景 里,两篇是配套的。
# 2.4 Reflect.has(obj, name)
对应 in 操作符,也对应 Proxy 的 has trap。
var obj = {
name: "poetries",
};
// 旧写法
'name' in obj // true
// 新写法
Reflect.has(obj, 'name') // true
注意 has 会沿着原型链查找,和 in 的行为一致。只想查自身属性得用 Object.prototype.hasOwnProperty.call(obj, 'name') 或者更现代的 Object.hasOwn(obj, 'name')。