JavaScript 中的 Proxy 与 Reflect 教程

4 阅读7分钟

JavaScript 中的 Proxy 与 Reflect 教程

目录

[TOC]

Proxy 是ES6引入的一种元编程特性。它允许创建一个代理对象来包装目标对象,并拦截对目标对象的各种操作(如属性读取、写入、枚举、函数调用等),从而可以自定义这些操作的行为。语法上, new Proxy(target, handler) 接受两个参数:第一个是要代理的目标对象,第二个是包含各种“捕获器”(traps)的处理器对象。例如, get 捕获器可以拦截属性读取操作, set 捕获器拦截属性写入操作等等。如果处理器中定义了对应的捕获器,就会在操作发生时被触发;否则,操作会默认直接作用于目标对象。Proxy 机制被广泛用于框架和库中,比如 Vue3 的响应式系统就是基于 Proxy 实现的

get 和 set 捕获器详解

handler 中可以定义不同的捕获器来拦截操作。其中最常用的是 getset

  • get(target, property, receiver) :拦截属性读取操作,当访问 proxy.property 时触发。参数说明为

  • target :目标对象(被代理的原始对象)。

  • property :被读取的属性名。

  • receiver :通常是当前访问该属性时的 this 值(通常就是代理对象本身,或者如果继承自该代理的对象,则是继承对象)。如果该属性是一个 getter,那么 receiver 就是执行 getter 时的 this

  • set(target, property, value, receiver) :拦截属性写入操作,当执行 proxy.property = value 时触发。参数说明为

target :目标对象。

  • property :被写入的属性名。

  • value :写入的值。

  • receiver :与 get 类似,对应 setter 被调用时的 this
    set 捕获器应该返回一个布尔值, true 表示写入成功, false 表示失败(失败时会抛出 TypeError

例如,以下代码演示了一个带有 getset 捕获器的 Proxy 用法:

const person = {
  name: "张三",
  get aliasName() {
    return this.name + "handsome";
  },
};
const proxyPerson = new Proxy(person, {
  get(target, key, receiver) {
    console.log("get", key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value) {
    console.log(`set ${key} = ${value}`);
    return Reflect.set(target, key, value);
  },
});
console.log(person.aliasName);       // 张三handsome
console.log(proxyPerson.aliasName);  // 输出:get aliasName \n 张三handsome
proxyPerson.name = "李四";           // 输出:set name = 李四
console.log(proxyPerson.aliasName);  // 输出:get aliasName \n 李四handsome

上例中, person.aliasName 会直接调用原对象的 getter,输出 "张三handsome" 。而访问 proxyPerson.aliasName 时, get 捕获器被触发,先打印日志,再调用 Reflect.get(target, key, receiver) 将操作转发给原对象,最终也输出 "张三handsome" 。同理,给 proxyPerson.name 赋新值时, set 捕获器被触发,并调用 Reflect.set 转发赋值。

为什么要用 Reflect?

Reflect 调用对象的基本方法「内部方法」,一个对象进行操作的时候都执行的是内部方法

在 JavaScript 中,对象的基本操作(如属性读取、设置、删除、函数调用等)实际上都是由一组标准的“ 内部方法 ”驱动的,比如:

  • [[Get]] :用于读取属性(如 obj.foo

  • [[Set]] :用于设置属性(如 obj.foo = 123

  • [[Has]] :用于判断属性是否存在(如 'foo' in obj

  • [[Delete]] :用于删除属性(如 delete obj.foo

这些内部方法是 JavaScript 引擎在底层实现的,开发者通常只能通过语法间接触发,无法直接调用。

从 ES6 开始, Reflect API 提供了一组函数,让你可以显式地、以函数形式调用这些内部方法。例如:

Reflect 方法等价语法对应内部方法
Reflect.get(obj, prop)obj[prop][[Get]]
Reflect.set(obj, prop, value)obj[prop] = value[[Set]]
Reflect.has(obj, prop)'prop' in obj[[Has]]
Reflect.deleteProperty(obj, prop)delete obj[prop][[Delete]]

这意味着, Reflect 可以直接操作对象的底层行为,并且与 Proxy 捕获器的接口完全一致 。这在代理对象中尤为重要:

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    // 显式调用目标对象的默认 get 行为,同时指定 this(receiver)
    return Reflect.get(target, prop, receiver);
  }
});

使用语法间接调用内部方法
const obj = {
  get value() {
    return 42;
  }
};
 
console.log(obj.value); // 间接调用 [[Get]] 内部方法

特点:

  • 使用的是“语法糖”。

  • JS 引擎背后会自动调用对应的内部方法,比如 [[Get]][[Set]][[Call]] 等。

  • 无法绕过一些限制(比如原型链、代理、setter/getter 等机制)。

使用 Reflect 直接调用内部方法
Reflect.get(obj, 'value'); // 直接调用 [[Get]],更接近内部机制

特点:

  • Reflect API 是对语言内部操作的一种“显式封装”。

  • 它更直接、更底层,能精确控制行为,适合写框架、库或者代理对象时使用。

  • 不触发一些 JS 隐式行为,比如不会触发原型链上其他逻辑(如果你用得当)。


对比总结:
维度使用语法(间接)Reflect(直接)
语义高层、面向对象底层、面向机制
控制力相对较少更强、更精细
触发器(如 Proxy)更可能被拦截更透明,适用于 Proxy
常用场景一般业务逻辑框架/库/元编程

Reflect API 及其与 Proxy 的配合

在 Proxy 的捕获器中使用 Reflect 可以方便地将操作转发给原对象。通常,如果我们在 get 捕获器里想保留目标对象的默认行为,只需使用相同参数调用 Reflect.get 即可,在 set 捕获器里使用 Reflect.set 同理。例如:

get(target, prop, receiver) {
  // 将读取操作转发给目标对象,并正确处理 this 绑定
  return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
  // 将写入操作转发给目标对象
  return Reflect.set(target, prop, value, receiver);
}

Reflect 的方法参数与捕获器的参数一致:正因为如此, Reflect.get(target, prop, receiver) 不仅读取了 target[prop] ,还会以 receiver 作为 this 去执行 getter,从而保持了原有的上下文,如果将 Reflect.get 简写为 target[prop] (不传入 receiver ),对于普通属性没区别,但在有继承或 getter 的情况下,会导致 this 指向问题。下面示例说明了这一点。

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};
let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) 没有使用 Reflect.get
  }
});
let admin = {
  __proto__: userProxy,
  _name: "Admin"
};
console.log(admin.name); // 期望 "Admin",实际输出 "Guest"

里我们通过原型继承让 admin 的原型链上有一个代理对象 userProxy 。当访问 admin.name 时,查找沿原型链找到 userProxy 并触发其 get 捕获器。捕获器返回 target[prop] ,即在原始对象 user 上读取 name 。由于 name 是一个 getter,这里以 target=user 作为 this 来执行,结果返回 user._name (即 "Guest" ),而不是 admin._name 。这样导致输出错误。

解决办法是在 get 捕获器中使用 Reflect.get 并传入第三个参数 receiver ,这样 getter 的 this 将指向正确的对象(此处为 admin ):

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver 会是 admin
    return Reflect.get(target, prop, receiver); // 传递 receiver,确保正确的 this
  }
});
console.log(admin.name); // Admin

正如上面所示, Reflect.get(target, prop, receiver) 会使用 receiver (即 admin )作为执行 getter 时的上下文,从而得到正确结果。实际上,所有 Proxy 捕获器都可以用相同参数调用对应的 Reflect 方法进行默认行为转发

另外需要注意的是,如果在捕获器中错误地对 receiver 对象进行 Reflect.get 操作,也会引发无限递归。例如:

let p = new Proxy(target, {
  get(raw, key, receiver) {
    if (key === 'b') return 3;
    // 错误示例:将操作作用于 receiver(代理本身)会导致死循环
    return Reflect.get(receiver, key);
  }
});
p.a; // 访问 p.a 时会重复调用 get 捕获器,导致死循环:contentReference[oaicite:24]{index=24}

在上例中,Reflect.get(receiver, key) 相当于在代理 p 上再次读取属性,从而再次触发相同的 get 捕获器,形成无限循环

正确做法应是使用 Reflect.get(target, key, receiver) 转发给原始对象。总之,在 Proxy 捕获器中使用 Reflect 进行操作转发是最安全可靠的方式

Proxy 的典型应用场景

Proxy 拦截操作的能力非常强大,可以用于多种实用场景,包括但不限于以下几类(示例均有出处参考):

  • 数据绑定 / 响应式 :在框架中常见,如 Vue3 使用 Proxy 对象实现响应式数据绑定,通过在 get / set 中拦截属性访问与赋值,可以自动触发视图更新。例如:

  • 在一个对象被包装为 Proxy 后,每当给属性赋值时, set 捕获器可以调用更新视图的函数,比如:将数据对象通过 Proxy 包装, set 拦截器中调用 updateUI() 来刷新界面。

  • Vue3 的 reactive() 函数内部就是创建了一个 Proxy 对象,对所有属性读写进行拦截,实现了深度响应,这样,当我们修改 state.count 时,框架会自动重新渲染相关组件。

  • 日志记录与调试 :可以用来监控对象的读写操作。例如,在 get 捕获器中打印访问的属性名,或在 set 捕获器中记录写入值。在前面的示例代码中,我们正是通过 console.log("get", key)console.log("set ...") 来监控 aliasNamename 属性的访问。虽然该示例用于数据绑定,但其 get 捕获器中调用了 console.log 以打印信息。 数据校验 / 保护 :在 set 捕获器中可以验证赋值的合法性,拒绝无效值。