代理
Proxy
对象使您能够为另一个对象创建代理,该代理可以拦截并重新定义该对象的基本操作。
描述
Proxy
对象允许您创建一个对象,该对象可以代替原始对象使用,但可以重新定义基本 Object
操作,如获取、设置和定义属性。代理对象通常用于记录属性访问、验证、格式化或清理输入等。
您可以使用两个参数创建 Proxy
target
:您想要代理的原始对象handler
:一个对象,用于定义将拦截哪些操作以及如何重新定义拦截的操作。
例如,此代码为 target
对象创建了一个代理。
const target = {
message1: "hello",
message2: "everyone",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
因为处理程序为空,所以此代理的行为与原始目标完全相同
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
要自定义代理,我们在处理程序对象上定义函数
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
在这里,我们提供了 get()
处理程序的实现,该处理程序拦截尝试访问目标中的属性。
处理程序函数有时称为陷阱,大概是因为它们会捕获对目标对象的调用。上面 handler2
中的非常简单的陷阱重新定义了所有属性访问器
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
代理通常与 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
方法调用再次被陷阱拦截,则可能会发生无限递归。
术语
对象内部方法
对象是属性的集合。但是,该语言没有提供任何机制来直接操作存储在对象中的数据——而是,对象定义了一些内部方法,指定了如何与之交互。例如,当您读取 obj.x
时,您可能期望发生以下情况
x
属性沿着原型链向上搜索,直到找到它。- 如果
x
是数据属性,则返回属性描述符的value
属性。 - 如果
x
是访问器属性,则调用 getter,并返回 getter 的返回值。
此过程中在语言中没有任何特殊之处——这仅仅是因为普通对象默认情况下具有一个 [[Get]]
内部方法,该方法使用此行为定义。obj.x
属性访问语法只是在对象上调用 [[Get]]
方法,并且对象使用其自己的内部方法实现来确定要返回什么。
再举一个例子,数组与普通对象不同,因为它们有一个神奇的length
属性,当修改该属性时,会自动分配空插槽或从数组中删除元素。同样,添加数组元素会自动更改 length
属性。这是因为数组具有一个 [[DefineOwnProperty]]
内部方法,该方法知道在写入整数索引时更新 length
,或者在写入 length
时更新数组内容。其内部方法与普通对象具有不同实现的此类对象称为奇异对象。Proxy
使开发人员能够定义自己的奇异对象,并具有全部功能。
所有对象都具有以下内部方法
内部方法 | 对应陷阱 |
---|---|
[[GetPrototypeOf]] |
getPrototypeOf() |
[[SetPrototypeOf]] |
setPrototypeOf() |
[[IsExtensible]] |
isExtensible() |
[[PreventExtensions]] |
preventExtensions() |
[[GetOwnProperty]] |
getOwnPropertyDescriptor() |
[[DefineOwnProperty]] |
defineProperty() |
[[HasProperty]] |
has() |
[[Get]] |
get() |
[[Set]] |
set() |
[[Delete]] |
deleteProperty() |
[[OwnPropertyKeys]] |
ownKeys() |
函数对象还具有以下内部方法
内部方法 | 对应陷阱 |
---|---|
[[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()
处理程序。
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
对象或任何具有内部槽的对象。有关更多信息,请参阅无私有属性转发。
无私有属性转发
代理仍然是另一个具有不同标识的对象——它是代理,在包装对象和外部之间运行。因此,代理无法直接访问原始对象的私有属性。
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
。
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
值重定向到原始对象。
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]]
的内部槽,用于存储映射的键值对。因此,您无法简单地为映射创建转发代理。
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
您必须使用上面说明的“this
恢复”代理来解决此问题。
验证
使用Proxy
,您可以轻松验证传递给对象的值。此示例使用set()
处理程序。
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
属性的对象的代理。代理处理程序定义了set()
处理程序。
当我们将 HTML 元素分配给view.selected
时,该元素的'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'
完整的陷阱列表示例
现在,为了创建完整的示例traps
列表,出于教学目的,我们将尝试代理化一个特别适合此类操作的非原生对象:由一个简单的 Cookie 框架创建的docCookies
全局对象。
/*
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);
规范
规范 |
---|
ECMAScript 语言规范 # sec-proxy-objects |
浏览器兼容性
BCD 表格仅在启用 JavaScript 的浏览器中加载。
另请参阅
- 代理很强大Brendan Eich 在 JSConf (2014) 上的演示文稿