先记住这个答案
Reflect.set 和普通赋值走的是同一个内部写入流程,查原型链、调 setter、检查属性是否可写,这些行为完全一样。不同点在于结果怎么告诉你:Reflect.set 返回布尔值,写入失败就返回 false;普通赋值失败时,严格模式下抛 TypeError,非严格模式下什么都不发生。另外 Reflect.set 的第四个参数 receiver 可以指定 setter 执行时的 this,这是它在 Proxy 里被大量使用的原因。
- 两者底层写入逻辑相同,都会触发 setter
Reflect.set失败返回false,不抛错- 赋值失败:严格模式抛错,非严格模式静默
receiver决定 setter 的this
写入失败时谁会告诉你
拿一个冻结的对象分别试试两种写法。示例运行在非严格模式下。
const frozen = Object.freeze({ x: 1 })
const r1 = (frozen.x = 2) // 赋值表达式的值就是右边的 2
const r2 = Reflect.set(frozen, 'x', 2) // 写入失败,返回 false
console.log(r1, r2, frozen.x)
const obj = { x: 1 }
console.log((obj.x = 5), Reflect.set(obj, 'x', 6), obj.x)查看输出与解释
2 false 1
5 true 6第一行里 frozen.x 没变,但赋值表达式照样得到 2,看不出失败。Reflect.set 直接返回 false。写入普通对象时,它返回 true,值也确实改了。
如果这段代码在严格模式下跑,frozen.x = 2 会直接抛 TypeError,后面的代码都执行不到。Reflect.set 在两种模式下表现一致,都只返回 false。它唯一会抛错的情况是第一个参数不是对象,比如 Reflect.set(1, 'x', 2)。
写入会失败的情况有这几种:属性不可写、属性是只有 getter 没有 setter 的访问器、对象不可扩展时新增属性,以及原型链上同名属性不可写。最后一种比较隐蔽,对象自己明明没有这个属性,却因为原型上的同名属性只读而写不进去。
receiver 参数改变了什么
赋值 obj.v = 9 时,setter 里的 this 就是 obj。Reflect.set(target, 'v', 9, receiver) 则是在 target 上找属性,但把 receiver 当作 this。
const target = {
_v: 1,
set v(val) { this._v = val } // this 是 receiver
}
const other = { _v: 0 }
Reflect.set(target, 'v', 9, other)
console.log(target._v, other._v)
const plain = { n: 1 }
const recv = {}
Reflect.set(plain, 'n', 2, recv) // 普通数据属性,值写到 receiver 上
console.log(plain.n, recv.n)查看输出与解释
1 9
1 2第一组里 setter 定义在 target 上,但执行时 this 是 other,所以改的是 other._v。第二组 n 是普通数据属性,值同样写到了 recv 上,plain 没变。这就是规范里的规则:写入最终落在 receiver 上,不传 receiver 时默认就是 target 本身。
- 为什么 Proxy 的
set陷阱要传receiver - Proxy 的
set陷阱签名是set(target, key, value, receiver),这里的receiver通常是代理对象本身,或者继承了代理的对象。在陷阱里写return Reflect.set(target, key, value, receiver),setter 里的this会指向代理,后续读写依然经过代理,响应式系统才能拦截到。直接写target[key] = value会让this变成原始对象,绕过了代理。而且陷阱必须返回布尔值,严格模式下返回false会抛TypeError,Reflect.set的返回值正好可以直接用。Vue 3 的reactive在set陷阱里就是这么写的。
容易答错的地方
- 以为
Reflect.set会绕过 setter 或只读检查 - 它不是「底层直接写内存」的后门,setter 照样触发,只读属性照样写不进去。想跳过 setter 定义一个数据属性,应该用
Object.defineProperty或Reflect.defineProperty。 - 以为赋值失败一定会报错
- 非严格模式下给只读属性赋值是静默失败的。老项目里的普通脚本不是严格模式,这类 bug 很难发现。ES Module 和
class内部默认是严格模式,会直接抛错。 - 用赋值表达式的结果判断成功与否
a.x = 2这个表达式的值永远是 2,和写入成没成功无关。要判断结果只能用Reflect.set的返回值,或者写完再读一次。
面试官还会怎么问?
Reflect 上的方法和 Object 上的同名方法有什么区别?
主要是失败时的处理方式。比如 Object.defineProperty 失败会抛 TypeError,Reflect.defineProperty 返回 false。另外 Reflect 的方法和 Proxy 的 13 个陷阱一一对应,参数也一致,所以在陷阱里转发默认行为最方便。
在 Proxy 的 set 陷阱里不返回值会怎样?
不返回就等于返回 undefined,会被当作 false。非严格模式下这次写入静默失败,严格模式下抛 TypeError。所以陷阱里一定要把 Reflect.set 的结果返回出去。
原型上有 setter,给子对象赋值会发生什么?
会调用原型上的 setter,this 指向子对象,子对象上不会新增同名属性。这和数据属性不同,原型上是可写的数据属性时,赋值会在子对象上新建一个同名属性,把原型上的遮住。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。