代理
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.
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:
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
为了自定义代理,我们在处理程序对象上定义函数:
¥To customize the proxy, we define functions on the handler object:
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:
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:
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.
对象内部方法
¥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, soProxy
instances do not have any special properties or methods.
静态方法
示例
基本示例
¥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.
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.
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.
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
:
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:
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:
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.
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
.
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.
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.
/*
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 |
浏览器兼容性
BCD tables only load in the browser
也可以看看
¥See also
- Brendan Eich 在 JSConf 上的 代理很棒 演示 (2014)