代理

Proxy 对象使你能够为另一个对象创建代理,该代理可以拦截并重新定义该对象的基本操作。

¥The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object.

描述

¥Description

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

¥The Proxy object allows you to create an object that can be used in place of the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties. Proxy objects are commonly used to log property accesses, validate, format, or sanitize inputs, and so on.

你创建一个带有两个参数的 Proxy

¥You create a Proxy with two parameters:

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

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

¥For example, this code creates a proxy for the target object.

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

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

因为处理程序是空的,所以该代理的行为就像原始目标一样:

¥Because the handler is empty, this proxy behaves just like the original target:

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

为了自定义代理,我们在处理程序对象上定义函数:

¥To customize the proxy, we define functions on the handler object:

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

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

const proxy2 = new Proxy(target, handler2);

在这里,我们提供了 get() 处理程序的实现,它拦截访问目标中的属性的尝试。

¥Here we've provided an implementation of the get() handler, which intercepts attempts to access properties in the target.

处理函数有时被称为陷阱,大概是因为它们捕获对目标对象的调用。上面 handler2 中非常简单的陷阱重新定义了所有属性访问器:

¥Handler functions are sometimes called traps, presumably because they trap calls to the target object. The very simple trap in handler2 above redefines all property accessors:

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

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

¥Proxies are often used with the Reflect object, which provides some methods with the same names as the Proxy traps. The Reflect methods provide the reflective semantics for invoking the corresponding object internal methods. For example, we can call Reflect.get if we don't wish to redefine the object's behavior:

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 方法仍然通过对象内部方法与对象交互 - 如果在代理上调用它,它不会 "de-proxify" 代理。如果你在代理陷阱中使用 Reflect 方法,并且 Reflect 方法调用再次被陷阱拦截,则可能会出现无限递归。

¥The Reflect method still interacts with the object through object internal methods — it doesn't "de-proxify" the proxy if it's invoked on a proxy. If you use Reflect methods within a proxy trap, and the Reflect method call gets intercepted by the trap again, there may be infinite recursion.

术语

¥Terminology

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

¥The following terms are used when talking about the functionality of proxies.

handler

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

trap

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

target

代理虚拟的对象。它通常用作代理的存储后端。针对对象验证有关对象不可扩展性或不可配置属性的不变量(保持不变的语义)。

invariants

实现自定义操作时语义保持不变。如果你的陷阱实现违反了处理程序的不变量,则会抛出 TypeError

对象内部方法

¥Object internal methods

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

¥Objects are collections of properties. However, the language doesn't provide any machinery to directly manipulate data stored in the object — rather, the object defines some internal methods specifying how it can be interacted with. For example, when you read obj.x, you may expect the following to happen:

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

该语言中的这个过程没有什么特别之处 - 只是因为默认情况下,普通对象有一个用这种行为定义的 [[Get]] 内部方法。obj.x 属性访问语法只是调用对象上的 [[Get]] 方法,并且对象使用其自己的内部方法实现来确定要返回的内容。

¥There isn't anything special about this process in the language — it's just because ordinary objects, by default, have a [[Get]] internal method that is defined with this behavior. The obj.x property access syntax simply invokes the [[Get]] method on the object, and the object uses its own internal method implementation to determine what to return.

另一个例子,arrays 与普通对象不同,因为它们有一个神奇的 length 属性,修改后会自动分配空槽或从数组中删除元素。同样,添加数组元素会自动更改 length 属性。这是因为数组有一个 [[DefineOwnProperty]] 内部方法,该方法知道在写入整数索引时更新 length,或者在写入 length 时更新数组内容。这种内部方法与普通对象有不同实现的对象称为奇异对象。Proxy 使开发者能够充分定义自己的奇异对象。

¥As another example, arrays differ from normal objects, because they have a magic length property that, when modified, automatically allocates empty slots or removes elements from the array. Similarly, adding array elements automatically changes the length property. This is because arrays have a [[DefineOwnProperty]] internal method that knows to update length when an integer index is written to, or update the array contents when length is written to. Such objects whose internal methods have different implementations from ordinary objects are called exotic objects. Proxy enable developers to define their own exotic objects with full capacity.

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

¥All objects have the following internal methods:

内部方法 对应陷阱
[[GetPrototypeOf]] getPrototypeOf()
[[SetPrototypeOf]] setPrototypeOf()
[[IsExtensible]] isExtensible()
[[PreventExtensions]] preventExtensions()
[[GetOwnProperty]] getOwnPropertyDescriptor()
[[DefineOwnProperty]] defineProperty()
[[HasProperty]] has()
[[Get]] get()
[[Set]] set()
[[Delete]] deleteProperty()
[[OwnPropertyKeys]] ownKeys()

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

¥Function objects also have the following internal methods:

内部方法 对应陷阱
[[Call]] apply()
[[Construct]] construct()

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

¥It's important to realize that all interactions with an object eventually boils down to the invocation of one of these internal methods, and that they are all customizable through proxies. This means almost no behavior (except certain critical invariants) is guaranteed in the language — everything is defined by the object itself. When you run delete obj.x, there's no guarantee that "x" in obj returns false afterwards — it depends on the object's implementations of [[Delete]] and [[HasProperty]]. A delete obj.x may log things to the console, modify some global state, or even define a new property instead of deleting the existing one, although these semantics should be avoided in your own code.

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

¥All internal methods are called by the language itself, and are not directly accessible in JavaScript code. The Reflect namespace offers methods that do little more than call the internal methods, besides some input normalization/validation. In each trap's page, we list several typical situations when the trap is invoked, but these internal methods are called in a lot of places. For example, array methods read and write to array through these internal methods, so methods like push() would also invoke get() and set() traps.

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

¥Most of the internal methods are straightforward in what they do. The only two that may be confusable are [[Set]] and [[DefineOwnProperty]]. For normal objects, the former invokes setters; the latter doesn't. (And [[Set]] calls [[DefineOwnProperty]] internally if there's no existing property or the property is a data property.) While you may know that the obj.x = 1 syntax uses [[Set]], and Object.defineProperty() uses [[DefineOwnProperty]], it's not immediately apparent what semantics other built-in methods and syntaxes use. For example, class fields use the [[DefineOwnProperty]] semantic, which is why setters defined in the superclass are not invoked when a field is declared on the derived class.

构造函数

¥Constructor

Proxy()

创建一个新的 Proxy 对象。

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

¥Note: There's no Proxy.prototype property, so Proxy instances do not have any special properties or methods.

静态方法

¥Static methods

Proxy.revocable()

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

示例

¥Examples

基本示例

¥Basic example

在这个简单的示例中,当属性名称不在对象中时,数字 37 将作为默认值返回。它正在使用 get() 处理程序。

¥In this simple example, the number 37 gets returned as the default value when the property name is not in the object. It is using the 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

无操作转发代理

¥No-op forwarding proxy

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

¥In this example, we are using a native JavaScript object to which our proxy will forward all operations that are applied to it.

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!)

请注意,虽然此 "no-op" 适用于纯 JavaScript 对象,但它不适用于原生对象,例如 DOM 元素、Map 对象或任何具有内部槽的对象。请参阅 没有私有属性转发 了解更多信息。

¥Note that while this "no-op" works for plain JavaScript objects, it does not work for native objects, such as DOM elements, Map objects, or anything that has internal slots. See no private property forwarding for more information.

没有私有属性转发

¥No private property forwarding

代理仍然是另一个具有不同身份的对象 - 它是在封装对象和外部对象之间运行的代理。因此,代理无法直接访问原始对象的 私有属性

¥A proxy is still another object with a different identity — it's a proxy that operates between the wrapped object and the outside. As such, the proxy does not have direct access to the original object's private properties.

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

const aSecret = new Secret("123456");
console.log(aSecret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(aSecret, {});
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

¥This is because when the proxy's get trap is invoked, the this value is the proxy instead of the original secret, so #secret is not accessible. To fix this, use the original secret as this:

js
const proxy = new Proxy(aSecret, {
  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 值重定向到原始对象:

¥For methods, this means you have to redirect the method's this value to the original object as well:

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

const aSecret = new Secret();
const proxy = new Proxy(aSecret, {
  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]] 的内部槽,它存储映射的键值对。因此,你不能简单地为地图创建转发代理:

¥Some native JavaScript objects have properties called internal slots, which are not accessible from JavaScript code. For example, Map objects have an internal slot called [[MapData]], which stores the key-value pairs of the map. As such, you cannot trivially create a forwarding proxy for a map:

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

你必须使用上面所示的“this-recovering”代理来解决此问题。

¥You have to use the "this-recovering" proxy illustrated above to work around this.

验证

¥Validation

使用 Proxy,你可以轻松验证对象传递的值。此示例使用 set() 处理程序。

¥With a Proxy, you can easily validate the passed value for an object. This example uses the 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 节点

¥Manipulating DOM nodes

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

¥In this example we use Proxy to toggle an attribute of two different elements: so when we set the attribute on one element, the attribute is unset on the other one.

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

¥We create a view object which is a proxy for an object with a selected property. The proxy handler defines the set() handler.

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

¥When we assign an HTML element to view.selected, the element's 'aria-selected' attribute is set to true. If we then assign a different element to view.selected, this element's 'aria-selected' attribute is set to true and the previous element's 'aria-selected' attribute is automatically set to 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

值修复和额外属性

¥Value correction and an extra property

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

¥The products proxy object evaluates the passed value and converts it to an array if needed. The object also supports an extra property called latestBrowser both as a getter and a 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'

完整的陷阱列表示例

¥A complete traps list example

现在,为了创建完整的示例 traps 列表,出于教学目的,我们将尝试代理特别适合此类操作的非原生对象:一个简单的 cookie 框架 创建的 docCookies 全局对象。

¥Now in order to create a complete sample traps list, for didactic purposes, we will try to proxify a non-native object that is particularly suited to this type of operation: the docCookies global object created by a simple cookie framework.

js
/*
  const docCookies = ... get the "docCookies" object here:
  https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/

const docCookies = new Proxy(docCookies, {
  get(target, key) {
    return target[key] ?? target.getItem(key) ?? undefined;
  },
  set(target, key, value) {
    if (key in target) {
      return false;
    }
    return target.setItem(key, value);
  },
  deleteProperty(target, key) {
    if (!(key in target)) {
      return false;
    }
    return target.removeItem(key);
  },
  ownKeys(target) {
    return target.keys();
  },
  has(target, key) {
    return key in target || target.hasItem(key);
  },
  defineProperty(target, key, descriptor) {
    if (descriptor && "value" in descriptor) {
      target.setItem(key, descriptor.value);
    }
    return target;
  },
  getOwnPropertyDescriptor(target, key) {
    const value = target.getItem(key);
    return value
      ? {
          value,
          writable: true,
          enumerable: true,
          configurable: false,
        }
      : undefined;
  },
});

/* Cookies test */

console.log((docCookies.myCookie1 = "First value"));
console.log(docCookies.getItem("myCookie1"));

docCookies.setItem("myCookie1", "Changed value");
console.log(docCookies.myCookie1);

规范

Specification
ECMAScript Language Specification
# sec-proxy-objects

¥Specifications

浏览器兼容性

BCD tables only load in the browser

¥Browser compatibility

也可以看看

¥See also