Proxy
Proxy 对象允许你为一个对象创建一个代理,以拦截并重新定义该对象的基本操作。
描述
Proxy 对象允许你创建一个可以替代原始对象使用的对象,但它可以重新定义对象的基本 Object 操作,如获取、设置和定义属性。Proxy 对象通常用于记录属性访问、验证、格式化或清理输入等。
你创建 Proxy 时需要两个参数
target:你想要代理的原始对象handler:一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。
例如,此代码创建了一个 target 对象的代理。
const target = {
message1: "hello",
message2: "everyone",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
因为 handler 是空的,所以这个代理的行为与原始 target 相同
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
要自定义代理,我们在 handler 对象上定义函数
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 中的陷阱重新定义了所有属性访问器。
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
Proxy 对象经常与 Reflect 对象一起使用,该对象提供了一些与 Proxy 陷阱同名的方法。Reflect 方法提供了调用相应 对象内部方法 的反射语义。例如,如果我们不想重新定义对象的行为,我们可以调用 Reflect.get。
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 方法调用被陷阱再次拦截,可能会发生无限递归。
术语
在讨论代理的功能时,会使用以下术语。
Object internal methods
Objects 是属性的集合。然而,语言本身并不提供任何机制来直接操作存储在对象中的数据——相反,对象定义了一些内部方法来指定如何与之交互。例如,当你读取 obj.x 时,你可能期望发生以下情况:
- 在 原型链 上向上搜索
x属性,直到找到为止。 - 如果
x是数据属性,则返回属性描述符的value属性。 - 如果
x是访问器属性,则调用 getter,并返回 getter 的返回值。
在这个过程中,语言本身并没有什么特别之处——这仅仅是因为普通对象默认具有一个具有此行为的 [[Get]] 内部方法。obj.x 属性访问语法只是调用对象上的 [[Get]] 方法,而对象使用自己的内部方法实现来确定返回什么。
再举个例子,数组与普通对象不同,因为它们有一个特殊的 length 属性,当修改该属性时,它会自动分配空槽或从数组中删除元素。同样,添加数组元素会自动更改 length 属性。这是因为数组有一个 [[DefineOwnProperty]] 内部方法,它知道在写入整数索引时更新 length,或者在写入 length 时更新数组内容。像这样的内部方法实现与普通对象不同的对象被称为exotic objects。Proxy 使开发者能够完全自定义此类对象。
所有对象都具有以下内部方法
| 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。
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 对象,我们的代理会将应用于该对象的所有操作转发给它。
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。
无私有字段转发
代理仍然是另一个具有不同身份的对象——它是包装对象和外部世界之间操作的代理。因此,代理无法直接访问原始对象的 私有元素。
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。
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 值重定向到原始对象。
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 创建一个转发代理。
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
你必须使用上面说明的“this 恢复”代理来解决这个问题。
验证
使用 Proxy,你可以轻松地验证传递给对象的数值。此示例使用 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 节点
在此示例中,我们使用 Proxy 来切换两个不同元素的属性:因此,当我们为一个元素设置属性时,该属性会在另一个元素上被取消设置。
我们创建一个 view 对象,它是具有 selected 属性的对象的代理。代理 handler 定义了 set() handler。
当我们为 view.selected 分配一个 HTML 元素时,该元素的 'aria-selected' 属性被设置为 true。如果我们随后为 view.selected 分配另一个元素,则该元素的 'aria-selected' 属性被设置为 true,而前一个元素的 'aria-selected' 属性则自动设置为 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
值校正和一个额外属性
products 代理对象会评估传入的值,并在需要时将其转换为数组。该对象还支持一个名为 latestBrowser 的额外属性,既可以作为 getter 也可以作为 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'
规范
| 规范 |
|---|
| ECMAScript® 2026 语言规范 # sec-proxy-objects |
浏览器兼容性
加载中…
另见
- Proxies are awesome Brendan Eich 在 JSConf (2014) 上的演讲