Proxy与Reflect

张玥 2026年9月17日 阅读时间 45分钟
ES6+ Proxy Reflect 元编程 响应式原理
ES6+新特性之Proxy与Reflect

JavaScript 从诞生之日起就被贴上了"动态"的标签,而 ES6 引入的 Proxy 与 Reflect 则把这种动态能力推向了新的高度——它们让开发者可以拦截并自定义对象的基本操作,真正踏入"元编程"的领域。Vue 3 的响应式系统、MobX、Immer、各类 ORM 与校验库,背后都有 Proxy 的身影。本文将从语言规范、13 个拦截器、Reflect 的镜像方法,一路讲到响应式系统实战、性能陷阱与工程最佳实践。45分钟沉浸式学习,带你彻底吃透这对"元编程双子星"。

从 defineProperty 到 Proxy:元编程的演进

在 Proxy 出现之前,JavaScript 想要"监听对象的读写"只有一条路:Object.defineProperty。它确实能劫持属性的 getter / setter,但代价与缺陷同样明显。

/* ES5 时代:使用 defineProperty 实现属性拦截 */
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; // 不会被拦截
defineProperty 的局限
  • 只能劫持已存在的属性
  • 新增 / 删除属性无法感知
  • 数组索引、length 需要特殊处理
  • 无法拦截 in、for...in、函数调用
Proxy 的能力
  • 代理整个对象,而非单个属性
  • 13 种基本操作全部可拦截
  • 对数组、函数、类天然友好
  • 可撤销、可嵌套、可组合

所谓元编程(Metaprogramming),就是"编写能够操作程序的程序"。在 JavaScript 中,Proxy 让我们可以介入语言最底层的对象操作流程,从而在不修改原对象的前提下,赋予它全新的行为语义。

Proxy 基本语法与核心模型

Proxy 的构造函数接收两个参数:目标对象(target)与处理器对象(handler)。所有对外部操作的拦截逻辑,都写在 handler 中。

/* Proxy 基本语法 */
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.name
→
拦截层
handler.get
→
真实数据
target.name

Proxy 就像在对象外面套了一层"透明的代理层"

关键认知: Proxy 创建的是一个"新对象",所有操作都必须通过 proxy 进行才会被拦截。直接操作 target 是绕过拦截的。同时,proxy 与 target 并不是同一个对象(proxy !== target),但改变 target 会反映到 proxy 上。

十三个拦截器(Traps)全解

Proxy 一共定义了 13 种可拦截的基本操作,覆盖了对象在语言层面的几乎所有行为。掌握它们,才算真正掌握了 Proxy。

/* 1. get —— 读取属性 */
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) {}
get
obj.prop
set
obj.prop = v
has
'p' in obj
deleteProperty
delete obj.p
ownKeys
Object.keys
apply
fn()
construct
new Fn()
getPrototypeOf
原型读取

13 个 trap = 对象行为的完整切面

其中 get、set、has、deleteProperty、ownKeys、apply、construct 是日常开发中使用频率最高的七个,下面我们逐一拆解。

get 与 set:最常用的两个拦截器

get 与 set 是 Proxy 使用频率最高的两个拦截器,也是构建响应式系统与数据校验的基石。它们的第三个参数 receiver 常常被忽略,却至关重要。

/* receiver:谁在调用这个 getter */
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,可能出错 */
get(target, prop, receiver)
target:被代理的原始对象
prop:被读取的属性名(string 或 symbol)
receiver:最初被调用的对象(通常是 proxy)
set(target, prop, value, receiver)
value:待写入的新值
返回值:必须返回布尔值,false 在严格模式下抛错

set 忘记 return true 是最常见的新手错误

Reflect:与 Proxy 天生一对

Reflect 是 ES6 同时引入的内置对象,它把语言内部的方法(如 [[Get]]、[[Set]])暴露为普通的函数调用。Reflect 的每一个方法,都恰好对应 Proxy 的一个拦截器,两者是完美的镜像关系。

/* Reflect 常用方法一览 */
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
为什么推荐用 Reflect
  • 函数式调用,不再是命令式语法
  • 返回值语义统一,便于判断成败
  • 自动传递正确的 receiver
  • 与 Proxy trap 一一对应,代码对称
经典对称写法
get(t, k, r) { return Reflect.get(t, k, r); }
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
写入流程
1. 业务代码执行 user.age = -1
2. 进入 handler.set 拦截
3. 校验失败 → 抛出 TypeError
4. 校验通过 → Reflect.set 写入

数据在源头就得到保护

实战二:手写一个响应式系统(Vue 3 原理)

Vue 3 抛弃了 Object.defineProperty,改用 Proxy 实现响应式。下面我们用一个约 40 行的迷你实现,还原其核心思想:读取时收集依赖,写入时触发更新。

/* 1. 依赖存储 */
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
响应式闭环
get → track 收集当前 effect
↓
WeakMap<target, Map<key, Set<effect>>>
↓
set → 值变化
↓
trigger → 重新执行 effect

这就是 Vue 3 reactivity 的核心骨架

为什么用 WeakMap? 因为 WeakMap 的键是弱引用,当 target 对象被回收时,对应的依赖表也会自动释放,避免内存泄漏。这是响应式库的标准做法。

实战三:只读代理、不可变数据与负数索引

Proxy 能实现许多过去需要深拷贝才能达到的效果,而且性能更好、语义更清晰。

/* 1. 只读代理 */
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]
只读代理
冻结整个对象树
负数索引
arr[-1] 取末位
默认值填充
访问不存在返回默认
访问统计
埋点与调试

Proxy 让"魔法语法"成为可能

apply 与 construct:拦截函数行为

Proxy 不仅能代理普通对象,还能代理函数。当 target 是函数时,apply 和 construct 两个拦截器就会生效。

/* 1. apply:拦截函数调用 */
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
apply(target, thisArg, argsList)
触发时机:fn()、fn.call()、fn.apply()
construct(target, argsList, newTarget)
触发时机:new Fn();返回值必须是对象

函数代理 = AOP(面向切面编程)的利器

常见陷阱与不变量约束

Proxy 虽然强大,但语言规范为它设定了严格的"不变量(Invariants)"。违反这些约束会直接抛出 TypeError。

/* 陷阱 1:不可配置属性不可"撒谎" */
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');

/* 结论:代理适合"写少读多"或"边界清晰"的场景 */
相对耗时(示意)
直接访问
defineProperty
Proxy 转发

具体数值随引擎版本与实现方式变化

优化建议:
  • 不要在超高频的循环体内使用代理对象
  • trap 内部尽量只做必要判断,减少闭包与深拷贝
  • 用 WeakMap 缓存代理结果,避免重复创建
  • 只代理真正需要拦截的属性,而非整个对象树
  • 在框架层使用(Vue、MobX),在业务层谨慎使用

Proxy vs Object.defineProperty 全面对比

理解了二者差异,就能明白为什么现代框架集体转向 Proxy。

对比维度
defineProperty
Proxy
拦截粒度
单个属性
整个对象
新增属性
无法感知
自动拦截
删除属性
无法拦截
deleteProperty
数组支持
需重写方法
原生支持
函数调用
不支持
apply / construct
遍历拦截
不支持
ownKeys
初始化开销
需遍历所有属性
一次创建即可
浏览器兼容
IE9+
无 IE 支持

工程最佳实践与总结

  • 永远用 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 提供"出口"。每一个 trap 都应该在完成自己的逻辑后,通过 Reflect 把操作交还给语言本身。这种"拦截—增强—放行"的模式,是元编程最优雅的写法。

写在最后

Proxy 与 Reflect 是 ES6 中最具"元"气质的两个特性。它们不是日常业务中天天要写的语法糖,却是理解现代前端框架、构建通用库与中间件的关键钥匙。

Proxy
在对象外面套一层拦截器,13 个 trap 覆盖对象的所有基本操作。
Reflect
把语言内部方法暴露为函数,与 Proxy 一一对应,是最佳拍档。

当你能熟练地用 Proxy 写出响应式系统、校验器、缓存代理、只读视图时,你就真正理解了 JavaScript 对象模型的底层运作方式。这才是"进阶"的意义所在。

恭喜完成 45 分钟深度学习
建议动手把文中的每个示例敲一遍,理解会深刻十倍。