JavaScript 从诞生之日起就被贴上了"动态"的标签,而 ES6 引入的 Proxy 与 Reflect 则把这种动态能力推向了新的高度——它们让开发者可以拦截并自定义对象的基本操作,真正踏入"元编程"的领域。Vue 3 的响应式系统、MobX、Immer、各类 ORM 与校验库,背后都有 Proxy 的身影。本文将从语言规范、13 个拦截器、Reflect 的镜像方法,一路讲到响应式系统实战、性能陷阱与工程最佳实践。45分钟沉浸式学习,带你彻底吃透这对"元编程双子星"。
从 defineProperty 到 Proxy:元编程的演进
在 Proxy 出现之前,JavaScript 想要"监听对象的读写"只有一条路:Object.defineProperty。它确实能劫持属性的 getter / setter,但代价与缺陷同样明显。
const obj = { name: '张三' };
Object.defineProperty(obj, 'name', {
get() {
console.log('读取 name');
return this._name;
},
set(value) {
console.log('写入 name');
this._name = value;
}
});
/* 缺陷 1:必须预先知道属性名 */
obj.age = 28; // 不会被拦截
/* 缺陷 2:无法拦截删除、in、遍历等操作 */
delete obj.name; // 不会被拦截
- 只能劫持已存在的属性
- 新增 / 删除属性无法感知
- 数组索引、length 需要特殊处理
- 无法拦截 in、for...in、函数调用
- 代理整个对象,而非单个属性
- 13 种基本操作全部可拦截
- 对数组、函数、类天然友好
- 可撤销、可嵌套、可组合
所谓元编程(Metaprogramming),就是"编写能够操作程序的程序"。在 JavaScript 中,Proxy 让我们可以介入语言最底层的对象操作流程,从而在不修改原对象的前提下,赋予它全新的行为语义。
Proxy 基本语法与核心模型
Proxy 的构造函数接收两个参数:目标对象(target)与处理器对象(handler)。所有对外部操作的拦截逻辑,都写在 handler 中。
const proxy = new Proxy(target, handler);
/* 一个完整的读写拦截示例 */
const target = { name: '张三', age: 28 };
const handler = {
get(target, prop, receiver) {
console.log(`[GET] ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[SET] ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
proxy.name; // [GET] name → '张三'
proxy.age = 30; // [SET] age = 30
target.age; // 30 —— 原对象真的被修改了
Proxy 就像在对象外面套了一层"透明的代理层"
proxy !== target),但改变 target 会反映到 proxy 上。
十三个拦截器(Traps)全解
Proxy 一共定义了 13 种可拦截的基本操作,覆盖了对象在语言层面的几乎所有行为。掌握它们,才算真正掌握了 Proxy。
get(target, prop, receiver) {}
/* 2. set —— 写入属性 */
set(target, prop, value, receiver) {}
/* 3. has —— in 运算符 */
has(target, prop) {}
/* 4. deleteProperty —— delete 运算符 */
deleteProperty(target, prop) {}
/* 5. ownKeys —— Object.keys / for...in */
ownKeys(target) {}
/* 6. getOwnPropertyDescriptor */
getOwnPropertyDescriptor(target, prop) {}
/* 7. defineProperty */
defineProperty(target, prop, descriptor) {}
/* 8. getPrototypeOf */
getPrototypeOf(target) {}
/* 9. setPrototypeOf */
setPrototypeOf(target, proto) {}
/* 10. isExtensible */
isExtensible(target) {}
/* 11. preventExtensions */
preventExtensions(target) {}
/* 12. apply —— 函数调用 */
apply(target, thisArg, argsList) {}
/* 13. construct —— new 运算符 */
construct(target, argsList, newTarget) {}
obj.prop
obj.prop = v
'p' in obj
delete obj.p
Object.keys
fn()
new Fn()
原型读取
13 个 trap = 对象行为的完整切面
其中 get、set、has、deleteProperty、ownKeys、apply、construct 是日常开发中使用频率最高的七个,下面我们逐一拆解。
get 与 set:最常用的两个拦截器
get 与 set 是 Proxy 使用频率最高的两个拦截器,也是构建响应式系统与数据校验的基石。它们的第三个参数 receiver 常常被忽略,却至关重要。
const parent = {
get value() {
return this._value;
}
};
const child = Object.create(parent);
child._value = 42;
const proxy = new Proxy(child, {
get(target, prop, receiver) {
// receiver 是 proxy 本身,而非 target
console.log(receiver === proxy); // true
return Reflect.get(target, prop, receiver);
}
});
proxy.value; // 42,this 正确指向 proxy
/* 若写成 Reflect.get(target, prop),this 会指向 target,可能出错 */
set 忘记 return true 是最常见的新手错误
Reflect:与 Proxy 天生一对
Reflect 是 ES6 同时引入的内置对象,它把语言内部的方法(如 [[Get]]、[[Set]])暴露为普通的函数调用。Reflect 的每一个方法,都恰好对应 Proxy 的一个拦截器,两者是完美的镜像关系。
Reflect.get(target, prop, receiver)
Reflect.set(target, prop, value, receiver)
Reflect.has(target, prop)
Reflect.deleteProperty(target, prop)
Reflect.ownKeys(target)
Reflect.getOwnPropertyDescriptor(target, prop)
Reflect.defineProperty(target, prop, desc)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, proto)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.apply(fn, thisArg, args)
Reflect.construct(Fn, args, newTarget)
/* 对比:Object.defineProperty 失败会抛错 */
try {
Object.defineProperty(1, 'a', {});
} catch (e) {
console.log('抛错了');
}
/* Reflect 失败只返回 false,更优雅 */
Reflect.defineProperty(1, 'a', {}); // false
- 函数式调用,不再是命令式语法
- 返回值语义统一,便于判断成败
- 自动传递正确的 receiver
- 与 Proxy trap 一一对应,代码对称
set(t, k, v, r) { return Reflect.set(t, k, v, r); }
"有 Proxy 的地方,就有 Reflect"
实战一:数据校验与类型约束
借助 set 拦截器,我们可以在数据写入前完成类型校验、范围检查、格式化处理,把校验逻辑从业务代码中彻底剥离。
function createValidator(target, validators) {
return new Proxy(target, {
set(target, prop, value) {
const validate = validators[prop];
if (validate) {
const [ok, msg] = validate(value);
if (!ok) throw new TypeError(msg);
}
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
throw new Error(`不允许删除属性 ${prop}`);
}
});
}
/* 使用 */
const user = createValidator({}, {
age: v => [typeof v === 'number' && v >= 0 && v <= 150, '年龄必须是 0-150 的数字'],
email: v => [/^[^@]+@[^@]+\.[^@]+$/.test(v), '邮箱格式不正确'],
name: v => [typeof v === 'string' && v.length > 0, '姓名不能为空']
});
user.age = 28; // ✅
user.email = 'a@b.com'; // ✅
user.age = -1; // ❌ TypeError: 年龄必须是 0-150 的数字
delete user.name; // ❌ Error: 不允许删除属性 name
数据在源头就得到保护
实战二:手写一个响应式系统(Vue 3 原理)
Vue 3 抛弃了 Object.defineProperty,改用 Proxy 实现响应式。下面我们用一个约 40 行的迷你实现,还原其核心思想:读取时收集依赖,写入时触发更新。
const targetMap = new WeakMap();
let activeEffect = null;
/* 2. 收集依赖 */
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if (!deps) depsMap.set(key, deps = new Set());
deps.add(activeEffect);
}
/* 3. 触发更新 */
function trigger(target, key) {
const deps = targetMap.get(target)?.get(key);
if (deps) deps.forEach(effect => effect());
}
/* 4. 创建响应式对象 */
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
const result = Reflect.get(target, key, receiver);
// 深层响应式:值为对象则递归代理
return typeof result === 'object' && result !== null
? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) trigger(target, key);
return result;
}
});
}
/* 5. 副作用注册 */
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
/* 6. 使用 */
const state = reactive({ count: 0, user: { name: '张三' } });
effect(() => {
console.log(`count 变为: ${state.count}`);
});
state.count++; // 自动打印:count 变为: 1
state.count = 100; // 自动打印:count 变为: 100
这就是 Vue 3 reactivity 的核心骨架
实战三:只读代理、不可变数据与负数索引
Proxy 能实现许多过去需要深拷贝才能达到的效果,而且性能更好、语义更清晰。
function readonly(target) {
return new Proxy(target, {
set() { throw new TypeError('对象是只读的'); },
deleteProperty() { throw new TypeError('对象是只读的'); },
defineProperty() { throw new TypeError('对象是只读的'); },
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
return typeof value === 'object' && value !== null
? readonly(value) : value;
}
});
}
/* 2. 数组负数索引:arr[-1] 取最后一个元素 */
function negativeIndexArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
const index = Number(prop);
return target[target.length + index];
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
const index = Number(prop);
target[target.length + index] = value;
return true;
}
return Reflect.set(target, prop, value, receiver);
}
});
}
const list = negativeIndexArray([1, 2, 3, 4, 5]);
list[-1]; // 5
list[-2]; // 4
list[-1] = 99;
list; // [1, 2, 3, 4, 99]
Proxy 让"魔法语法"成为可能
apply 与 construct:拦截函数行为
Proxy 不仅能代理普通对象,还能代理函数。当 target 是函数时,apply 和 construct 两个拦截器就会生效。
function sum(a, b) { return a + b; }
const loggedSum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`调用参数: ${args.join(', ')}`);
const result = Reflect.apply(target, thisArg, args);
console.log(`返回结果: ${result}`);
return result;
}
});
loggedSum(3, 4);
// 调用参数: 3, 4
// 返回结果: 7
/* 2. 记忆化(缓存函数结果) */
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('命中缓存');
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
/* 3. construct:拦截 new 调用 */
class User {
constructor(name) { this.name = name; }
}
const UserProxy = new Proxy(User, {
construct(target, args, newTarget) {
console.log(`正在创建实例,参数: ${args.join(', ')}`);
return Reflect.construct(target, args, newTarget);
}
});
const u = new UserProxy('张三');
// 正在创建实例,参数: 张三
u instanceof User; // true
fn()、fn.call()、fn.apply()
new Fn();返回值必须是对象
函数代理 = AOP(面向切面编程)的利器
常见陷阱与不变量约束
Proxy 虽然强大,但语言规范为它设定了严格的"不变量(Invariants)"。违反这些约束会直接抛出 TypeError。
const target = {};
Object.defineProperty(target, 'a', {
value: 1,
configurable: false,
writable: false
});
const p = new Proxy(target, {
get(t, k) { return 999; }
});
p.a; // ❌ TypeError: 违反不变量
/* 陷阱 2:内置对象(Map / Set / Date)需要特殊处理 */
const map = new Map();
const badProxy = new Proxy(map, {
get(t, k) { return t[k]; } // ❌ this 指向错误
});
// badProxy.set('a', 1); → TypeError: Method Map.prototype.set called on incompatible receiver
/* 正确做法:绑定 this 或用 Reflect 传 receiver */
const goodProxy = new Proxy(map, {
get(t, k, receiver) {
const value = Reflect.get(t, k, receiver);
return typeof value === 'function'
? value.bind(t) : value;
}
});
goodProxy.set('a', 1); // ✅ 正常工作
- 无法拦截私有字段(
#field) - 无法代理
WeakMap等内部槽对象 - 目标对象被
Object.freeze后约束更多 typeof proxy仍为'object'- 存在一定性能开销
- 始终用 Reflect 转发默认行为
- 内置对象需绑定 this
- 热点路径避免高频代理
- 用
Proxy.revocable及时撤销
此外,Proxy.revocable() 可以创建一个可撤销的代理,调用 revoke() 后代理立即失效,非常适合用在临时授权、沙箱等场景:
const { proxy, revoke } = Proxy.revocable({ secret: '数据' }, {});
proxy.secret; // '数据'
revoke(); // 撤销代理
proxy.secret; // ❌ TypeError: Cannot perform 'get' on a proxy that has been revoked
性能分析与选型建议
Proxy 的拦截能力来自运行时的方法调用,这意味着它比直接属性访问要慢。理解性能特征,才能做出正确的技术选型。
const N = 1_000_000;
const raw = { value: 0 };
const proxied = new Proxy(raw, {
get(t, k, r) { return Reflect.get(t, k, r); },
set(t, k, v, r) { return Reflect.set(t, k, v, r); }
});
/* 直接访问:约 2-5 ms */
console.time('raw');
for (let i = 0; i < N; i++) raw.value++;
console.timeEnd('raw');
/* 代理访问:约为直接访问的 5-20 倍耗时 */
console.time('proxy');
for (let i = 0; i < N; i++) proxied.value++;
console.timeEnd('proxy');
/* 结论:代理适合"写少读多"或"边界清晰"的场景 */
具体数值随引擎版本与实现方式变化
- 不要在超高频的循环体内使用代理对象
- trap 内部尽量只做必要判断,减少闭包与深拷贝
- 用
WeakMap缓存代理结果,避免重复创建 - 只代理真正需要拦截的属性,而非整个对象树
- 在框架层使用(Vue、MobX),在业务层谨慎使用
Proxy vs Object.defineProperty 全面对比
理解了二者差异,就能明白为什么现代框架集体转向 Proxy。
工程最佳实践与总结
- 永远用 Reflect 转发默认行为:保持语义一致,避免遗漏 receiver
- 牢记 set 必须返回布尔值:返回 false 在严格模式下会抛错
- 注意不变量约束:不可配置、不可写属性不能被"欺骗"
- 内置对象要绑定 this:Map、Set、Date 等需要特殊处理
- 避免代理整个对象树:按需惰性代理,防止性能雪崩
- 用 WeakMap 缓存代理:同一个 target 复用同一个 proxy
- 区分"数据代理"与"行为代理":对象用 get/set,函数用 apply/construct
- 调试时打印 target:DevTools 中 proxy 的展示可能不直观
- 生产环境注意 polyfill 成本:Proxy 无法被完美 polyfill
const proxyCache = new WeakMap();
function createReactive(target, hooks = {}) {
if (proxyCache.has(target)) {
return proxyCache.get(target);
}
const proxy = new Proxy(target, {
get(t, key, receiver) {
const value = Reflect.get(t, key, receiver);
hooks.onGet?.(key, value);
if (value && typeof value === 'object') {
return createReactive(value, hooks);
}
return value;
},
set(t, key, value, receiver) {
const oldValue = t[key];
const ok = Reflect.set(t, key, value, receiver);
if (ok && oldValue !== value) {
hooks.onSet?.(key, value, oldValue);
}
return ok;
},
deleteProperty(t, key) {
const ok = Reflect.deleteProperty(t, key);
if (ok) hooks.onDelete?.(key);
return ok;
}
});
proxyCache.set(target, proxy);
return proxy;
}
/* 使用 */
const state = createReactive({ count: 0, info: { name: '张三' } }, {
onGet: (k, v) => console.log(`读取 ${k}`),
onSet: (k, v) => console.log(`更新 ${k} → ${v}`)
});
state.count++; // 读取 count / 更新 count → 1
state.info.name = '李四'; // 深层响应
写在最后
Proxy 与 Reflect 是 ES6 中最具"元"气质的两个特性。它们不是日常业务中天天要写的语法糖,却是理解现代前端框架、构建通用库与中间件的关键钥匙。
当你能熟练地用 Proxy 写出响应式系统、校验器、缓存代理、只读视图时,你就真正理解了 JavaScript 对象模型的底层运作方式。这才是"进阶"的意义所在。