ES6 引入的 Proxy 和 Reflect 为 JavaScript 带来了元编程能力。简单来说,Proxy 让你可以在对象外面包一层“拦截器”,任何对对象的操作都会先经过这层拦截;Reflect 则提供了一套与 Proxy 陷阱对应的默认操作方法,让拦截逻辑更容易回归默认行为。这两者结合,直接推动了现代前端框架(如 Vue 3)响应式系统的升级。
11.5.1 Proxy 的本质:可编程的对象代理
Proxy 构造函数接收两个参数:目标对象(target)和处理器对象(handler)。处理器对象中定义了一系列陷阱(trap),这些陷阱会在对应操作发生时被自动调用。
const target = {
name: '张三',
age: 25
};
const handler = {
// 拦截属性读取
get(target, property, receiver) {
console.log(`正在读取属性: ${property}`);
return target[property];
},
// 拦截属性设置
set(target, property, value, receiver) {
console.log(`正在设置属性: ${property} = ${value}`);
target[property] = value;
return true; // 严格模式下需要返回true表示成功
}
};
const proxy = new Proxy(target, handler);
proxy.name; // 控制台输出: 正在读取属性: name
proxy.age = 26; // 控制台输出: 正在设置属性: age = 26
对代理对象的任何操作,都会先触发对应的陷阱函数,开发者可以在此加入自定义逻辑(如校验、日志、拦截、转换),然后再决定是否将操作转交给目标对象。
11.5.2 13 种拦截操作
Proxy 支持 13 种陷阱,几乎覆盖了对象所有可能的行为:
| 陷阱方法 | 拦截的操作 |
|---------|-----------|
| get(target, property, receiver) | 读取属性:proxy.foo、proxy['foo'] |
| set(target, property, value, receiver) | 设置属性:proxy.foo = value |
| has(target, property) | in 运算符:'foo' in proxy |
| deleteProperty(target, property) | delete 操作:delete proxy.foo |
| ownKeys(target) | Object.keys()、for...in、Object.getOwnPropertyNames() 等 |
| getOwnPropertyDescriptor(target, property) | Object.getOwnPropertyDescriptor() |
| defineProperty(target, property, descriptor) | Object.defineProperty() |
| preventExtensions(target) | Object.preventExtensions() |
| isExtensible(target) | Object.isExtensible() |
| getPrototypeOf(target) | Object.getPrototypeOf() |
| setPrototypeOf(target, proto) | Object.setPrototypeOf() |
| apply(target, thisArg, argumentsList) | 函数调用:proxy(...args)、proxy.apply(), proxy.call() |
| construct(target, argumentsList, newTarget) | new 操作:new proxy(...args) |
通过这些陷阱,可以实现很多实用功能:属性访问日志、校验层、默认值、私有属性、数据绑定等。
11.5.3 Reflect:让默认行为更优雅
Reflect 是一个内置对象,它的方法与 Proxy 陷阱一一对应。引入 Reflect 的主要目的不是提供新功能,而是让默认操作更易用,尤其是与 Proxy 搭配时:
const obj = { a: 1 };
// 旧方式:用 in 判断属性存在
console.log('a' in obj); // true
// Reflect 方式
console.log(Reflect.has(obj, 'a')); // true
// 旧方式:用 delete 删除属性
delete obj.a;
// Reflect 方式
Reflect.deleteProperty(obj, 'a');
Reflect 的优势在于:
- 将原本分散的操作符(
in、delete)统一为函数调用,更符合函数式风格。 - 在
Proxy陷阱内部,直接使用Reflect方法调用默认行为,避免手动调用目标对象的繁琐错误处理。
一个标准(且安全)的 Proxy 陷阱写法:
const handler = {
set(target, property, value, receiver) {
// 自定义校验
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('年龄必须是数字');
}
// 调用默认行为,Reflect.set 返回布尔值指示是否成功
return Reflect.set(target, property, value, receiver);
}
};
11.5.4 响应式系统的实现原理
现代前端框架的核心挑战之一就是追踪数据变化并自动更新视图。Vue 3 的响应式系统(基于 Proxy)便是绝佳的应用实例。
原理概览:
- 将目标对象包装在一个
Proxy中。 - 在
get陷阱中收集依赖(哪个视图组件依赖了哪个属性),建立映射。 - 在
set陷阱中触发所有依赖此属性的视图更新。
简化版实现:
// 依赖收集与触发
const bucket = new WeakMap(); // target → (key → Set<effect>)
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn(); // 执行一次,触发 get,收集依赖
activeEffect = null;
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖
const depsMap = bucket.get(target);
if (depsMap) {
const deps = depsMap.get(key);
if (deps) {
deps.forEach(fn => fn());
}
}
return result;
}
});
}
// 使用
const state = reactive({ count: 0 });
effect(() => {
console.log(`count 的值是: ${state.count}`);
// 第一次输出: count 的值是: 0
});
state.count++; // 触发 set,自动输出: count 的值是: 1
这就是 Vue 3 响应式的骨架。实际框架中还需处理深层对象、数组、Map/Set 等,但核心思路不变:Proxy 让“拦截并自动响应对象操作”成为可能。
11.5.5 与 Object.defineProperty 的对比优势
在 ES6 之前,Vue 2 使用 Object.defineProperty 实现响应式,它通过修改属性的 getter/setter 来拦截读写操作。相比 Proxy,它的局限很明显:
| 对比维度 | Object.defineProperty | Proxy |
|---------|----------------------|-------|
| 能拦截的操作 | 只能拦截现有属性的 get/set | 可拦截 13 种操作,包括 in、delete、for...in、函数调用等 |
| 动态添加属性 | 无法拦截新增属性(需使用 Vue.set) | 天生支持,任何新增属性的访问都经过 get/set 陷阱 |
| 数组操作 | 无法直接拦截索引赋值、length 变化、push 等方法(需要重写数组方法) | 可以完美拦截数组索引赋值和 length 变化,对原型方法也有效 |
| 性能 | 直接修改属性描述符,初始化时需递归遍历 | 代理整个对象,惰性收集依赖,性能更优 |
| 兼容性 | ES5,兼容性好 | ES6,完全无法在旧浏览器使用 |
典型场景对比:
// Object.defineProperty 的痛点
const obj = { a: 1 };
Object.defineProperty(obj, 'a', {
get() { console.log('get a'); return trueValue; },
set(val) { console.log('set a'); trueValue = val; }
});
obj.a = 2; // 正常触发
obj.b = 3; // 无法拦截!obj.b 变成了普通属性
delete obj.a; // 无法拦截!
// Proxy 完美解决
const p = new Proxy({ a: 1 }, {
get(target, key) { console.log(`get ${key}`); return target[key]; },
set(target, key, val) { console.log(`set ${key}`); target[key] = val; return true; },
deleteProperty(target, key) { console.log(`delete ${key}`); return delete target[key]; }
});
p.b = 3; // 触发 set 陷阱,输出 "set b"
delete p.a; // 触发 deleteProperty 陷阱,输出 "delete a"
由于 Proxy 本质上是架构在对象之外的“代理层”,无需侵入原始对象的结构,因此它的能力范围更广,实现也更优雅。
11.5.6 实际应用场景
除了响应式系统,Proxy 还广泛应用于:
- 数据校验:在
set中校验类型、范围、格式,拒绝非法赋值。 - 私有属性保护:通过
has、get、ownKeys等隐藏以下划线开头的属性。 - 日志与性能监控:记录对象操作日志,统计属性访问频率。
- 缓存与懒加载:重载
get实现惰性加载(如大型对象延迟初始化)。 - DOM 更新代理:将 DOM 操作映射为对象属性修改,实现数据绑定。
- 安全的权限控制:限制某些属性的读写或调用。
11.5.7 注意事项
Proxy不能完全模拟原始对象的行为,特别是在身份判断(===)和原型链比较时,代理对象不等于原始对象。Proxy需要与Reflect配合使用,尤其是当陷阱需要保证返回预期结果时(如set必须返回布尔值)。- 对性能敏感的场景,避免过深的
Proxy嵌套或高频率的陷阱操作。 Proxy无法被 polyfill,只能在支持 ES6 的环境中使用。在 Vue 3 等框架中,已经不再兼容 IE 的一个很大原因就是Proxy的硬性要求。
Proxy 与 Reflect 的组合代表了 JavaScript 在元编程能力上的飞跃,理解它们不仅能让你读懂现代框架的底层实现,也能在合适的业务场景中亲手构建高度动态、可扩展的系统。