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;    // TypeError

has:隐藏属性(对 in 和 with 不可见)

const secret = new Proxy({ _token: "abc", name: "a" }, {
  has: (t, k) => !k.startsWith("_"),
});
"name" in secret;     // true
"_token" in secret;   // false

apply / 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) 后返回 3

ownKeys + 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 同名同参,作用是:

  1. 标准转发:在 trap 内用 Reflect.get(target, key, receiver) 保持默认语义(包括 getter 的 this 指向)
  2. 返回操作成败:Reflect.set() 返回 boolean,比 try/catch 优雅
  3. 取代 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];   // 30

3. 可撤销代理(外部强制失效,防泄露)

const { proxy, revoke } = Proxy.revocable(secretObj, {});
revoke();
proxy.x;   // TypeError: Cannot perform 'get' on a proxy that has been revoked

4. 本地对象伪装成 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)与坑

  1. 不可撒谎规则:对不可扩展对象/不可配置属性,trap 不能伪造结果。例如 target 上的不可配置不可写属性,get 必须返回真实值;ownKeys 结果必须包含所有不可配置键,否则直接抛 TypeError
  2. set 返回 false:严格模式下抛 TypeError,非严格模式静默失败——校验型代理应直接抛错或返回 true
  3. this 指向:直接 t[k] 调方法时 this 是 target;要让 getter/方法看到 proxy,用 Reflect.get(t, k, receiver) 并传 receiver
  4. 身份语义不变:proxy !== target,Map/Set 以原始引用为键时查不到代理;instanceof 依赖原型链时可能被 getPrototypeOf trap 影响
  5. 性能:每次属性访问多一层间接,热路径(每帧渲染、大循环)慎用
  6. 调试:代理对象在调试器里显示为 Proxy,栈帧里看到的是 trap 而非原操作

要点速记

  1. new Proxy(target, handler),13 个 trap 覆盖读/写/枚举/原型/调用全部基本操作
  2. 转发一律走 Reflect.*,别手写 target[key](丢 receiver、丢语义)
  3. 三大经典应用:响应式(Vue 3)、校验/默认值、枚举隐藏
  4. 注意不变量约束与 set 返回值语义,避免代理自身引入隐蔽 bug