JavaScript Proxy原理

0 阅读4分钟

什么是 Proxy

一句话概括:Proxy 是 JavaScript 中的一个“元编程”特性,它允许你创建一个对象的“代理”或“看门人”,从而可以拦截并自定义该对象上的基本操作(如读取、设置属性等)。

想象一下,你有一个很重要的对象 user

const user = {
  name: "张三",
  age: 30
};

正常情况下,你可以直接访问和修改它: console.log(user.name); // 读取 user.age = 31; // 修改

Proxy 就像是在这个 user 对象前面安插了一个全能的门卫。从此以后,任何对 user 的访问和修改都必须先经过这个门卫。而你,作为门卫的制定者,可以规定门卫在每次操作发生时应该做什么。


Proxy 的核心三要素

创建一个代理需要三个东西:

  1. target (目标对象):就是你想要代理的那个原始对象(比如上面的 user)。
  2. handler (处理器对象):一个配置对象,里面定义了当各种操作发生时,门卫(代理)应该执行的自定义行为。
  3. trap (陷阱函数)handler 对象中的一个个方法,比如 getset 等,它们就是用来“捕获”特定操作的“陷阱”。

一个简单的代码示例

让我们用代码来创建一个门卫。

// 1. 目标对象 (The VIP you want to protect)
const targetUser = {
  name: "Alice",
  age: 25
};

// 2. 处理器对象 (The rulebook for the doorman)
const handler = {
  // 'get' 是一个陷阱 (trap),它会在读取属性时被触发
  get(target, property) {
    console.log(`有人正在读取属性 '${property}'...`);
    // 必须返回原始值,否则读取操作会得到 undefined
    return target[property];
  },

  // 'set' 是另一个陷阱,它会在设置属性时被触发
  set(target, property, value) {
    console.log(`有人正在设置属性 '${property}' 为 '${value}'...`);
    
    // 我们可以在这里添加验证逻辑
    if (property === 'age' && typeof value !== 'number') {
      throw new TypeError("年龄必须是一个数字!");
    }

    // 执行原始的设置操作
    target[property] = value;

    // set 陷阱需要返回一个布尔值表示操作是否成功
    return true;
  }
};

// 3. 创建代理实例
const proxyUser = new Proxy(targetUser, handler);

// --- 现在,请注意:我们所有的操作都应该对 proxyUser 进行,而不是 targetUser ---

// 读取操作
console.log(proxyUser.name);
// 控制台输出:
// 有人正在读取属性 'name'...
// Alice

// 设置操作
proxyUser.age = 26;
// 控制台输出:
// 有人正在设置属性 'age' 为 '26'...

console.log(proxyUser.age);
// 控制台输出:
// 有人正在读取属性 'age'...
// 26

// 尝试一个非法的设置操作
try {
  proxyUser.age = "二十七";
} catch (e) {
  console.error(e.message);
}
// 控制台输出:
// 有人正在设置属性 'age' 为 '二十七'...
// 年龄必须是一个数字!

在这个例子中:

  • 我们对 proxyUser 的任何读取都会被 handler.get 捕获。
  • 我们对 proxyUser 的任何写入都会被 handler.set 捕获,并且我们还加入了验证逻辑。

Proxy 为何如此强大?(以及为什么 Vue 3 选择它)

Proxy 的强大之处在于它代理的是整个对象,而不是对象的某个属性。这解决了 Vue 2 中 Object.defineProperty 的核心痛点。

对比 Object.defineProperty (Vue 2 的方式):

  • 一次只能监听一个属性:你必须遍历对象的所有属性,为每个属性都调用一次 Object.defineProperty
  • 无法监听新增/删除属性:如果给对象动态添加一个新属性,Object.defineProperty 是不知道的,所以这个新属性不是响应式的。Vue 2 为此不得不提供了 Vue.setVue.delete 这样的补丁API。
  • 无法直接监听数组索引和长度的变化:对数组通过索引修改(如 arr[0] = ...)或修改 length 属性,Object.defineProperty 同样无能为力。Vue 2 需要重写数组的 push, pop, splice 等方法来hack式地实现监听。

Proxy 的优势 (Vue 3 的方式):

  1. 代理整个对象new Proxy(target, ...) 一行代码就代理了整个对象,性能更好,代码也更简洁。
  2. 天然支持新增/删除属性:因为代理的是整个对象,当你给代理对象添加或删除属性时,get/set/deleteProperty 等陷阱会自然地被触发,无需任何额外API。
  3. 天然支持数组操作:对数组的任何索引访问、赋值、修改 length 属性,都会被 Proxy 完美捕获。
  4. 提供更多陷阱Proxy 提供了多达13种陷阱,可以拦截各种各样的操作,远不止 getset。例如:
    • has(target, prop):拦截 prop in proxy 操作。
    • deleteProperty(target, prop):拦截 delete proxy[prop] 操作。
    • apply(target, thisArg, args):拦截函数的调用。
    • construct(target, args):拦截 new proxy() 操作。

总结

Proxy 是 JavaScript 提供的一个底层元编程接口,它像一个强大的“拦截器”,让我们能够重写对象几乎所有的内部方法。

对于 Vue 3 来说,Proxy 是实现其响应式系统的完美工具。它让 Vue 能够:

  • 高效地创建一个完全响应式的对象。
  • get 陷阱中收集依赖(谁读取了这个数据)。
  • setdeleteProperty 等陷阱中触发更新(通知所有依赖它的地方进行更新)。

并且这一切都以一种更现代、更全面、更符合 JavaScript 语言直觉的方式完成,彻底告别了 Object.defineProperty 时代的各种限制和“补丁”。