Proxy 和 Reflect 是什么?它们通常怎么配合使用?
简化版
Proxy 用来拦截对象的读取、赋值、删除、函数调用等底层操作;Reflect 提供与这些操作对应的标准方法,常在拦截器里完成默认行为,避免手写对象操作带来的语义偏差。
详细版
Proxy 创建对象代理:
const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
常见拦截点包括 get、set、has、deleteProperty、apply、construct。
Reflect 的方法名和代理陷阱基本对应,例如 Reflect.get、Reflect.set。
工程中它们常用于响应式系统、权限校验、数据校验、日志埋点和 API 包装。
面试重点:Proxy 是代理层,Reflect 是标准化默认操作;Vue 3 响应式就是高频应用场景。
完整版教学
一、Proxy 解决的是对象操作拦截问题
普通对象的属性读取和赋值默认是直接发生的。
如果希望在读取时收集依赖、赋值时触发更新、删除时校验权限,就需要一个统一拦截层。
Proxy 正是这个拦截层。
const target = { count: 1 };
const state = new Proxy(target, {
get(obj, key) {
console.log('read', key);
return obj[key];
},
set(obj, key, value) {
console.log('write', key, value);
obj[key] = value;
return true;
}
});
state.count; // read count
state.count = 2; // write count 2
这个例子里 1 次读取和 1 次赋值都被代理捕获。 它说明 Proxy 不是复制对象,而是在目标对象外面包了一层操作入口。
二、Reflect 负责把默认行为标准化
在代理陷阱中直接写 target[key] 有时不够准确。
比如 getter 中的 this、原型链上的属性访问、赋值是否成功等语义,都需要遵守语言规范。
Reflect 提供了一组与底层操作对应的方法,适合在陷阱中调用默认行为。
const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
}
});
| 操作 | Proxy 陷阱 | Reflect 方法 |
|---|---|---|
| 读取属性 | get | Reflect.get |
| 设置属性 | set | Reflect.set |
| 删除属性 | deleteProperty | Reflect.deleteProperty |
| 函数调用 | apply | Reflect.apply |
| new 调用 | construct | Reflect.construct |
可以把 Reflect 看成“默认动作按钮”。 你先在 Proxy 里做额外逻辑,再交给 Reflect 完成原本该发生的行为。
三、receiver 参数是很多追问的关键
get(target, key, receiver) 中的 receiver 通常指最初触发属性访问的代理对象。
当属性是 getter,并且 getter 内部使用 this 时,receiver 会影响 this 指向。
如果忽略它,代理对象和原型链组合时可能出现错误行为。
const obj = {
name: 'proxy',
get label() {
return this.name;
}
};
const proxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
在简单对象上你可能看不出区别。 但响应式系统要处理嵌套对象、原型链和 getter,必须尽量保持语言默认语义。 这也是 Vue 3 源码里大量使用 Reflect 的原因之一。
四、Proxy 可以拦截的范围比 Object.defineProperty 更大
Object.defineProperty 主要劫持已有属性的 getter/setter。
Proxy 代理的是对象操作,可以感知新增属性、删除属性、in 判断、枚举等更多行为。
这使它更适合做完整响应式系统。
读取 obj.a
-> get
新增 obj.b = 2
-> set
删除 delete obj.a
-> deleteProperty
'a' in obj
-> has
| 维度 | Object.defineProperty | Proxy |
|---|---|---|
| 新增属性 | 需要额外处理 | 可被 set 捕获 |
| 删除属性 | 难直接捕获 | 可被 deleteProperty 捕获 |
| 数组索引/长度 | 处理复杂 | 拦截更统一 |
| 兼容性 | 更老浏览器可用 | 旧环境不支持且难 polyfill |
Proxy 的能力更强,但兼容性成本也更高。
五、Proxy 的限制也要讲清楚
Proxy 只能代理对象,不能直接代理原始值。 代理对象和目标对象不是同一个引用,身份比较会不同。 某些内建对象或私有字段也可能有特殊限制。
const target = {};
const proxy = new Proxy(target, {});
console.log(proxy === target); // false
如果一个系统里同时暴露 target 和 proxy,就可能出现状态绕过。 例如直接改 target 不经过代理层,响应式依赖就可能收不到通知。 工程里通常要避免把原始对象和代理对象混着传。
Proxy 是“入口控制”,不是“对象变身”;绕过入口直接碰 target,代理逻辑就不会执行。
六、典型应用:响应式、校验和权限控制
响应式系统会在 get 时收集依赖,在 set 时触发更新。
数据校验会在 set 时检查类型、范围和只读规则。
权限控制可以在 get 或 deleteProperty 中阻止访问敏感字段。
const user = new Proxy({ age: 18 }, {
set(target, key, value, receiver) {
if (key === 'age' && value < 0) {
throw new Error('age must be positive');
}
return Reflect.set(target, key, value, receiver);
}
});
如果 age = -1,代理会在赋值真正发生前拦截。
如果 age = 20,再交给 Reflect.set 完成标准赋值。
这就是“增强操作但保留默认语义”的典型模式。
七、常见误区与追问
- 误区:Proxy 会复制一份 target。 Proxy 只是创建代理入口,内部仍关联原始 target,并不会自动深拷贝数据。
- 误区:Reflect 只是 Object 方法的别名。 Reflect 更贴近语言内部操作,并与 Proxy 陷阱一一配合。
- 误区:Proxy 能被完整 polyfill。 Proxy 拦截的是语言底层操作,旧环境很难用普通 JS 完整模拟。
- 追问:为什么
set陷阱要返回布尔值? 它表示赋值是否成功,严格模式下返回false可能触发错误。 - 追问:Vue 3 为什么用 Proxy 做响应式? 它能统一拦截新增、删除、数组和集合等操作,覆盖范围比
defineProperty更广。 - 追问:receiver 有什么用? 它让 Reflect 在 getter 或原型链场景中保持正确的
this和访问语义。
八、加强记忆
Proxy 记成“拦截入口”,Reflect 记成“默认动作”。回答时按三个层次展开:先说 Proxy 能拦哪些操作,再说 Reflect 为什么常用于陷阱里,最后补工程场景和限制,尤其是响应式、兼容性、target 与 proxy 身份不同。