人人都会AI编程

11.5 Proxy 与 Reflect

更新时间:2026-07-11

ES6 引入的 ProxyReflect 为 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.fooproxy['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...inObject.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 的优势在于:

  • 将原本分散的操作符(indelete)统一为函数调用,更符合函数式风格。
  • 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)便是绝佳的应用实例。

原理概览

  1. 将目标对象包装在一个 Proxy 中。
  2. get 陷阱中收集依赖(哪个视图组件依赖了哪个属性),建立映射。
  3. 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 种操作,包括 indeletefor...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 中校验类型、范围、格式,拒绝非法赋值。
  • 私有属性保护:通过 hasgetownKeys 等隐藏以下划线开头的属性。
  • 日志与性能监控:记录对象操作日志,统计属性访问频率。
  • 缓存与懒加载:重载 get 实现惰性加载(如大型对象延迟初始化)。
  • DOM 更新代理:将 DOM 操作映射为对象属性修改,实现数据绑定。
  • 安全的权限控制:限制某些属性的读写或调用。

11.5.7 注意事项

  • Proxy 不能完全模拟原始对象的行为,特别是在身份判断(===)和原型链比较时,代理对象不等于原始对象。
  • Proxy 需要与 Reflect 配合使用,尤其是当陷阱需要保证返回预期结果时(如 set 必须返回布尔值)。
  • 对性能敏感的场景,避免过深的 Proxy 嵌套或高频率的陷阱操作。
  • Proxy 无法被 polyfill,只能在支持 ES6 的环境中使用。在 Vue 3 等框架中,已经不再兼容 IE 的一个很大原因就是 Proxy 的硬性要求。

ProxyReflect 的组合代表了 JavaScript 在元编程能力上的飞跃,理解它们不仅能让你读懂现代框架的底层实现,也能在合适的业务场景中亲手构建高度动态、可扩展的系统。