Proxy

Baseline 已广泛支持

此特性已非常成熟,可在多种设备和浏览器版本上使用。自 ⁨2016 年 9 月⁩以来,它已在各大浏览器中可用。

Proxy 对象允许你为一个对象创建一个代理,以拦截并重新定义该对象的基本操作。

描述

Proxy 对象允许你创建一个可以替代原始对象使用的对象,但它可以重新定义对象的基本 Object 操作,如获取、设置和定义属性。Proxy 对象通常用于记录属性访问、验证、格式化或清理输入等。

你创建 Proxy 时需要两个参数

  • target:你想要代理的原始对象
  • handler:一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。

例如,此代码创建了一个 target 对象的代理。

js
const target = {
  message1: "hello",
  message2: "everyone",
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

因为 handler 是空的,所以这个代理的行为与原始 target 相同

js
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone

要自定义代理,我们在 handler 对象上定义函数

js
const target = {
  message1: "hello",
  message2: "everyone",
};

const handler2 = {
  get(target, prop, receiver) {
    return "world";
  },
};

const proxy2 = new Proxy(target, handler2);

在这里,我们提供了 get() handler 的实现,它会拦截对 target 中属性的访问尝试。

Handler 函数有时被称为陷阱 (traps),大概是因为它们会捕获对目标对象的调用。上面 handler2 中的陷阱重新定义了所有属性访问器。

js
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world

Proxy 对象经常与 Reflect 对象一起使用,该对象提供了一些与 Proxy 陷阱同名的方法。Reflect 方法提供了调用相应 对象内部方法 的反射语义。例如,如果我们不想重新定义对象的行为,我们可以调用 Reflect.get

js
const target = {
  message1: "hello",
  message2: "everyone",
};

const handler3 = {
  get(target, prop, receiver) {
    if (prop === "message2") {
      return "world";
    }
    return Reflect.get(...arguments);
  },
};

const proxy3 = new Proxy(target, handler3);

console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world

Reflect 方法仍然通过对象内部方法与对象交互——如果它在代理上调用,它不会“反代理”代理。如果你在代理陷阱中使用 Reflect 方法,并且 Reflect 方法调用被陷阱再次拦截,可能会发生无限递归。

术语

在讨论代理的功能时,会使用以下术语。

handler

传递给 Proxy 构造函数的第二个参数对象。它包含了定义代理行为的陷阱。

trap

定义相应 对象内部方法 行为的函数。(这类似于操作系统中的陷阱概念。)

目标

Proxy 虚拟化的对象。它通常用作代理的存储后端。关于对象不可扩展性或不可配置属性的不变性(sematics that remain unchanged)会针对目标对象进行验证。

invariants

在实现自定义操作时保持不变的语义。如果你的陷阱实现违反了 handler 的不变性,将会抛出一个 TypeError

Object internal methods

Objects 是属性的集合。然而,语言本身并不提供任何机制来直接操作存储在对象中的数据——相反,对象定义了一些内部方法来指定如何与之交互。例如,当你读取 obj.x 时,你可能期望发生以下情况:

  • 原型链 上向上搜索 x 属性,直到找到为止。
  • 如果 x 是数据属性,则返回属性描述符的 value 属性。
  • 如果 x 是访问器属性,则调用 getter,并返回 getter 的返回值。

在这个过程中,语言本身并没有什么特别之处——这仅仅是因为普通对象默认具有一个具有此行为的 [[Get]] 内部方法。obj.x 属性访问语法只是调用对象上的 [[Get]] 方法,而对象使用自己的内部方法实现来确定返回什么。

再举个例子,数组与普通对象不同,因为它们有一个特殊的 length 属性,当修改该属性时,它会自动分配空槽或从数组中删除元素。同样,添加数组元素会自动更改 length 属性。这是因为数组有一个 [[DefineOwnProperty]] 内部方法,它知道在写入整数索引时更新 length,或者在写入 length 时更新数组内容。像这样的内部方法实现与普通对象不同的对象被称为exotic objectsProxy 使开发者能够完全自定义此类对象。

所有对象都具有以下内部方法

Internal method Corresponding trap
[[GetPrototypeOf]] getPrototypeOf()
[[SetPrototypeOf]] setPrototypeOf()
[[IsExtensible]] isExtensible()
[[PreventExtensions]] preventExtensions()
[[GetOwnProperty]] getOwnPropertyDescriptor()
[[DefineOwnProperty]] defineProperty()
[[HasProperty]] has()
[[Get]] get()
[[Set]] set()
[[Delete]] deleteProperty()
[[OwnPropertyKeys]] ownKeys()

函数对象也具有以下内部方法

Internal method Corresponding trap
[[Call]] apply()
[[Construct]] construct()

需要认识到的是,所有与对象的交互最终都会归结为调用其中一个内部方法,并且所有这些方法都可以通过代理进行自定义。这意味着语言中几乎没有行为(除了一些关键的不变性)是确定的——一切都由对象本身定义。当你执行 delete obj.x 时,无法保证 "x" in obj 之后会返回 false——这取决于对象对 [[Delete]][[HasProperty]] 的实现。delete obj.x 可能会向控制台记录信息、修改全局状态,甚至定义一个新属性而不是删除现有属性,尽管在你的代码中应该避免这种语义。

所有内部方法都由语言本身调用,并且不能在 JavaScript 代码中直接访问。 Reflect 命名空间提供的方法除了进行一些输入归一化/验证之外,几乎不做什么,只是调用内部方法。在每个陷阱的页面中,我们列出了一些陷阱被调用的典型情况,但这些内部方法在大量地方被调用。例如,数组方法通过这些内部方法读写数组,因此像 push() 这样的方法也会调用 get()set() 陷阱。

大多数内部方法都很直接。唯一可能令人混淆的是 [[Set]][[DefineOwnProperty]]。对于普通对象,前者调用 setter;后者不调用。(如果不存在现有属性或属性是数据属性,则 [[Set]] 会在内部调用 [[DefineOwnProperty]]。)虽然你可能知道 obj.x = 1 语法使用 [[Set]],而 Object.defineProperty() 使用 [[DefineOwnProperty]],但其他内置方法和语法所使用的语义并不明显。例如,类字段使用 [[DefineOwnProperty]] 语义,这就是为什么在派生类上声明字段时,不会调用在超类中定义的 setter。

构造函数

Proxy()

创建一个新的 Proxy 对象。

注意: 没有 Proxy.prototype 属性,因此 Proxy 实例没有特殊的属性或方法。

静态方法

Proxy.revocable()

创建一个可撤销的 Proxy 对象。

示例

基本示例

在此示例中,当属性名称不在对象中时,数字 37 将作为默认值返回。它使用了 get() handler。

js
const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined

console.log("c" in p, p.c); // false, 37

无操作转发代理

在此示例中,我们使用一个原生的 JavaScript 对象,我们的代理会将应用于该对象的所有操作转发给它。

js
const target = {};
const p = new Proxy(target, {});

p.a = 37; // Operation forwarded to the target

console.log(target.a); // 37 (The operation has been properly forwarded!)

请注意,虽然这种“无操作”对于纯 JavaScript 对象有效,但对于原生对象(如 DOM 元素、Map 对象或任何具有内部槽的对象)无效。有关更多信息,请参阅 no private field forwarding

无私有字段转发

代理仍然是另一个具有不同身份的对象——它是包装对象和外部世界之间操作的代理。因此,代理无法直接访问原始对象的 私有元素

js
class Secret {
  #secret;
  constructor(secret) {
    this.#secret = secret;
  }
  get secret() {
    return this.#secret.replace(/\d+/, "[REDACTED]");
  }
}

const secret = new Secret("123456");
console.log(secret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(secret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it

这是因为当调用代理的 get 陷阱时,this 的值是 proxy 而不是原始的 secret,因此无法访问 #secret。要解决此问题,请使用原始的 secret 作为 this

js
const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    // By default, it looks like Reflect.get(target, prop, receiver)
    // which has a different value of `this`
    return target[prop];
  },
});
console.log(proxy.secret);

对于方法来说,这意味着你还需要将方法的 this 值重定向到原始对象。

js
class Secret {
  #x = 1;
  x() {
    return this.#x;
  }
}

const secret = new Secret();
const proxy = new Proxy(secret, {
  get(target, prop, receiver) {
    const value = target[prop];
    if (value instanceof Function) {
      return function (...args) {
        return value.apply(this === receiver ? target : this, args);
      };
    }
    return value;
  },
});
console.log(proxy.x());

一些原生的 JavaScript 对象具有称为内部槽的属性,这些属性无法从 JavaScript 代码访问。例如,Map 对象具有一个名为 [[MapData]] 的内部槽,它存储 map 的键值对。因此,你无法轻易地为 map 创建一个转发代理。

js
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy

你必须使用上面说明的“this 恢复”代理来解决这个问题。

验证

使用 Proxy,你可以轻松地验证传递给对象的数值。此示例使用 set() handler。

js
const validator = {
  set(obj, prop, value) {
    if (prop === "age") {
      if (!Number.isInteger(value)) {
        throw new TypeError("The age is not an integer");
      }
      if (value > 200) {
        throw new RangeError("The age seems invalid");
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  },
};

const person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = "young"; // Throws an exception
person.age = 300; // Throws an exception

操作 DOM 节点

在此示例中,我们使用 Proxy 来切换两个不同元素的属性:因此,当我们为一个元素设置属性时,该属性会在另一个元素上被取消设置。

我们创建一个 view 对象,它是具有 selected 属性的对象的代理。代理 handler 定义了 set() handler。

当我们为 view.selected 分配一个 HTML 元素时,该元素的 'aria-selected' 属性被设置为 true。如果我们随后为 view.selected 分配另一个元素,则该元素的 'aria-selected' 属性被设置为 true,而前一个元素的 'aria-selected' 属性则自动设置为 false

js
const view = new Proxy(
  {
    selected: null,
  },
  {
    set(obj, prop, newVal) {
      const oldVal = obj[prop];

      if (prop === "selected") {
        if (oldVal) {
          oldVal.setAttribute("aria-selected", "false");
        }
        if (newVal) {
          newVal.setAttribute("aria-selected", "true");
        }
      }

      // The default behavior to store the value
      obj[prop] = newVal;

      // Indicate success
      return true;
    },
  },
);

const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");

// select item1:
view.selected = item1;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true

// selecting item2 de-selects item1:
view.selected = item2;

console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false

console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true

值校正和一个额外属性

products 代理对象会评估传入的值,并在需要时将其转换为数组。该对象还支持一个名为 latestBrowser 的额外属性,既可以作为 getter 也可以作为 setter。

js
const products = new Proxy(
  {
    browsers: ["Firefox", "Chrome"],
  },
  {
    get(obj, prop) {
      // An extra property
      if (prop === "latestBrowser") {
        return obj.browsers[obj.browsers.length - 1];
      }

      // The default behavior to return the value
      return obj[prop];
    },
    set(obj, prop, value) {
      // An extra property
      if (prop === "latestBrowser") {
        obj.browsers.push(value);
        return true;
      }

      // Convert the value if it is not an array
      if (typeof value === "string") {
        value = [value];
      }

      // The default behavior to store the value
      obj[prop] = value;

      // Indicate success
      return true;
    },
  },
);

console.log(products.browsers);
//  ['Firefox', 'Chrome']

products.browsers = "Safari";
//  pass a string (by mistake)

console.log(products.browsers);
//  ['Safari'] <- no problem, the value is an array

products.latestBrowser = "Edge";

console.log(products.browsers);
//  ['Safari', 'Edge']

console.log(products.latestBrowser);
//  'Edge'

规范

规范
ECMAScript® 2026 语言规范
# sec-proxy-objects

浏览器兼容性

另见