ES2015 引入的元编程/反射能力。Proxy 在目标对象外套一层拦截器,劫持 13 种基本操作;Reflect 提供与这 13 个 trap 一一对应的静态方法,作为”标准转发姿势”。详见 globals-js-builtins 第 11 节速览。
基本结构
const proxy = new Proxy(target, handler);
// target:被代理的对象(任意对象/数组/函数)
// handler:陷阱集合,没定义的陷阱走默认行为(等价直接操作 target)trap 签名统一为 (target, ...args),默认实现用 Reflect.xxx(target, ...args) 转发。
13 个 trap 全表
| trap | 拦截操作 | 触发场景 |
|---|---|---|
get | 读属性 | obj.x、obj[x]、解构 |
set | 写属性 | obj.x = v |
has | 属性存在 | x in obj、with |
deleteProperty | 删除 | delete obj.x |
ownKeys | 枚举键 | Object.keys、for...in、Object.getOwnPropertyNames |
getOwnPropertyDescriptor | 取描述符 | Object.getOwnPropertyDescriptor |
defineProperty | 定义属性 | Object.defineProperty |
preventExtensions | 禁扩展 | Object.preventExtensions |
isExtensible | 可扩判断 | Object.isExtensible |
getPrototypeOf | 读原型 | Object.getPrototypeOf、instanceof 链 |
setPrototypeOf | 改原型 | Object.setPrototypeOf |
apply | 函数调用 | fn()、fn.call() |
construct | 构造调用 | new Fn() |
常用 trap 示例
get:默认值 / 未知属性报错
const config = new Proxy({}, {
get(t, key) {
if (!(key in t)) throw new ReferenceError(`配置项 ${String(key)} 未定义`);
return t[key];
},
});
config.port = 3000;
config.port; // 3000
config.host; // ReferenceError: 配置项 host 未定义set:类型校验(必须返回 true,严格模式下返回 false 会抛 TypeError)
const user = new Proxy({}, {
set(t, key, v) {
if (key === "age" && (!Number.isInteger(v) || v < 0))
throw new TypeError("age 必须是非负整数");
t[key] = v;
return true;
},
});
user.age = 18; // OK
user.age = -1; // TypeErrorhas:隐藏属性(对 in 和 with 不可见)
const secret = new Proxy({ _token: "abc", name: "a" }, {
has: (t, k) => !k.startsWith("_"),
});
"name" in secret; // true
"_token" in secret; // falseapply / construct:包装函数
function sum(a, b) { return a + b; }
const logged = new Proxy(sum, {
apply(fn, thisArg, args) {
console.log(`call(${args.join(",")})`);
return Reflect.apply(fn, thisArg, args); // 转发调用
},
construct(fn, args) {
return Reflect.construct(fn, args); // 拦截 new
},
});
logged(1, 2); // 打印 call(1,2) 后返回 3ownKeys + getOwnPropertyDescriptor:控制枚举
Object.keys(proxy) 同时走这两个 trap,需配合过滤:
const hideUnderscore = (obj) => new Proxy(obj, {
ownKeys: (t) => Reflect.ownKeys(t).filter((k) => !String(k).startsWith("_")),
getOwnPropertyDescriptor: (t, k) => Reflect.getOwnPropertyDescriptor(t, k),
});
Object.keys(hideUnderscore({ a: 1, _b: 2 })); // ["a"]Reflect 的角色
13 个静态方法与 13 个 trap 同名同参,作用是:
- 标准转发:在 trap 内用
Reflect.get(target, key, receiver)保持默认语义(包括 getter 的this指向) - 返回操作成败:
Reflect.set()返回 boolean,比try/catch优雅 - 取代 Object 上的怪癖:
Reflect.defineProperty返回 boolean(Object.defineProperty抛错),Reflect.deleteProperty替代delete运算符
get(t, key, receiver) {
// receiver 是当前 proxy,保证 getter 里 this 指向 proxy 而非 target
return Reflect.get(t, key, receiver);
}实战模式
1. Vue 3 风格响应式(依赖收集 + 懒代理)
const cache = new WeakMap(); // target -> proxy,防重复包装
function reactive(obj) {
if (cache.has(obj)) return cache.get(obj);
const p = new Proxy(obj, {
get(t, k, r) {
track(t, k); // 收集依赖
const v = Reflect.get(t, k, r);
return typeof v === "object" && v ? reactive(v) : v; // 惰性深层代理
},
set(t, k, v, r) {
const ok = Reflect.set(t, k, v, r);
trigger(t, k); // 触发更新
return ok;
},
});
cache.set(obj, p);
return p;
}2. 数组负索引(Python 风格)
const arr = new Proxy([10, 20, 30], {
get(t, k, r) {
if (typeof k === "string" && /^-\d+$/.test(k)) k = String(t.length + Number(k));
return Reflect.get(t, k, r);
},
});
arr[-1]; // 303. 可撤销代理(外部强制失效,防泄露)
const { proxy, revoke } = Proxy.revocable(secretObj, {});
revoke();
proxy.x; // TypeError: Cannot perform 'get' on a proxy that has been revoked4. 本地对象伪装成 RPC 客户端
const remote = new Proxy({}, {
get: (_t, method) => (...args) =>
fetch("/rpc", { method: "POST", body: JSON.stringify({ method, args }) }).then(r => r.json()),
});
await remote.getUser(1); // 自动转成远程调用5. 函数结果缓存(memoize)
const memoize = (fn) => {
const cache = new Map();
return new Proxy(fn, {
apply(t, thisArg, args) {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, Reflect.apply(t, thisArg, args));
return cache.get(key);
},
});
};不变量(invariants)与坑
- 不可撒谎规则:对不可扩展对象/不可配置属性,trap 不能伪造结果。例如 target 上的不可配置不可写属性,
get必须返回真实值;ownKeys结果必须包含所有不可配置键,否则直接抛 TypeError - set 返回 false:严格模式下抛 TypeError,非严格模式静默失败——校验型代理应直接抛错或返回 true
- this 指向:直接
t[k]调方法时this是 target;要让 getter/方法看到 proxy,用Reflect.get(t, k, receiver)并传receiver - 身份语义不变:
proxy !== target,Map/Set 以原始引用为键时查不到代理;instanceof依赖原型链时可能被getPrototypeOftrap 影响 - 性能:每次属性访问多一层间接,热路径(每帧渲染、大循环)慎用
- 调试:代理对象在调试器里显示为 Proxy,栈帧里看到的是 trap 而非原操作
要点速记
new Proxy(target, handler),13 个 trap 覆盖读/写/枚举/原型/调用全部基本操作- 转发一律走
Reflect.*,别手写target[key](丢 receiver、丢语义) - 三大经典应用:响应式(Vue 3)、校验/默认值、枚举隐藏
- 注意不变量约束与
set返回值语义,避免代理自身引入隐蔽 bug