一个 Proxy
对象包装另一个对象并拦截操作,例如读取/写入属性和其他操作,可以选择自行处理它们,或透明地允许该对象处理它们。
代理在许多库和一些浏览器框架中使用。我们将在本文中看到许多实际应用。
代理
语法
let proxy = new Proxy(target, handler)
target
– 是要包装的对象,可以是任何东西,包括函数。handler
– 代理配置:一个包含“陷阱”的对象,这些方法会拦截操作。– 例如,get
陷阱用于读取target
的属性,set
陷阱用于将属性写入target
,等等。
对于 proxy
上的操作,如果 handler
中存在相应的陷阱,则它会运行,并且代理有机会处理它,否则操作将在 target
上执行。
作为入门示例,让我们创建一个没有任何陷阱的代理
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 5; // writing to proxy (1)
alert(target.test); // 5, the property appeared in target!
alert(proxy.test); // 5, we can read it from proxy too (2)
for(let key in proxy) alert(key); // test, iteration works (3)
由于没有陷阱,proxy
上的所有操作都将转发到 target
。
- 写入操作
proxy.test=
将值设置在target
上。 - 读取操作
proxy.test
返回target
中的值。 - 遍历
proxy
返回target
中的值。
正如我们所见,在没有陷阱的情况下,proxy
是 target
的透明包装器。
Proxy
是一种特殊的“奇异对象”。它没有自己的属性。使用空的 handler
,它会透明地将操作转发到 target
。
为了激活更多功能,让我们添加陷阱。
我们可以用它们拦截什么?
对于对象上的大多数操作,在 JavaScript 规范中都有一个所谓的“内部方法”,它描述了在最低级别是如何工作的。例如 [[Get]]
,读取属性的内部方法,[[Set]]
,写入属性的内部方法,等等。这些方法只在规范中使用,我们不能直接按名称调用它们。
Proxy 陷阱拦截了对这些方法的调用。它们列在 Proxy 规范 和下面的表格中。
对于每个内部方法,表格中都有一个陷阱:我们可以添加到 new Proxy
的 handler
参数中的方法名称,用于拦截操作。
内部方法 | Handler 方法 | 触发时间… |
---|---|---|
[[Get]] |
get |
读取属性 |
[[Set]] |
set |
写入属性 |
[[HasProperty]] |
has |
in 运算符 |
[[Delete]] |
deleteProperty |
delete 运算符 |
[[Call]] |
apply |
函数调用 |
[[Construct]] |
construct |
new 运算符 |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty,Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor,for..in ,Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames,Object.getOwnPropertySymbols,for..in ,Object.keys/values/entries |
JavaScript 强制执行一些不变式 - 内部方法和陷阱必须满足的条件。
大多数是针对返回值的
[[Set]]
如果值写入成功,则必须返回true
,否则返回false
。[[Delete]]
如果值成功删除,则必须返回true
,否则返回false
。- …等等,我们将在下面的示例中看到更多。
还有一些其他的不变式,比如
[[GetPrototypeOf]]
,应用于代理对象必须返回与应用于代理对象目标对象的[[GetPrototypeOf]]
相同的值。换句话说,读取代理的原型必须始终返回目标对象的原型。
陷阱可以拦截这些操作,但它们必须遵循这些规则。
不变式确保语言特性的正确和一致的行为。完整的不变式列表在 规范 中。如果你没有做奇怪的事情,你可能不会违反它们。
让我们看看它在实际示例中的工作原理。
使用“get”陷阱的默认值
最常见的陷阱是用于读取/写入属性。
要拦截读取,handler
应该有一个方法 get(target, property, receiver)
。
它在读取属性时触发,带有以下参数
target
- 是目标对象,作为第一个参数传递给new Proxy
,property
- 属性名称,receiver
- 如果目标属性是 getter,则receiver
是将在其调用中用作this
的对象。通常是proxy
对象本身(或者如果我们从代理继承,则是一个从它继承的对象)。现在我们不需要这个参数,所以稍后会详细解释。
让我们使用 get
为对象实现默认值。
我们将创建一个数字数组,它对不存在的值返回 0
。
通常,当有人尝试获取一个不存在的数组项时,他们会得到 undefined
,但我们将一个普通数组包装到代理中,该代理会拦截读取并返回 0
如果没有这样的属性
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // default value
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such item)
正如我们所看到的,使用 get
陷阱很容易做到。
我们可以使用 Proxy
为“默认”值实现任何逻辑。
假设我们有一个字典,包含短语及其翻译
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
现在,如果没有短语,从 dictionary
读取将返回 undefined
。但在实践中,将短语保留为未翻译通常比 undefined
更好。因此,让我们在那种情况下让它返回一个未翻译的短语,而不是 undefined
。
为了实现这一点,我们将 dictionary
包装在一个代理中,该代理会拦截读取操作
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercept reading a property from dictionary
if (phrase in target) { // if we have it in the dictionary
return target[phrase]; // return the translation
} else {
// otherwise, return the non-translated phrase
return phrase;
}
}
});
// Look up arbitrary phrases in the dictionary!
// At worst, they're not translated.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation)
请注意代理如何覆盖变量
dictionary = new Proxy(dictionary, ...);
代理应该完全替换目标对象。在代理目标对象后,任何人都不能再引用目标对象。否则很容易弄乱。
使用“set”陷阱进行验证
假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,应该出现错误。
当写入属性时,set
陷阱会触发。
set(target, property, value, receiver)
:
target
- 是目标对象,作为第一个参数传递给new Proxy
,property
- 属性名称,value
– 属性值,receiver
– 与get
陷阱类似,仅对 setter 属性有效。
如果设置成功,set
陷阱应该返回 true
,否则返回 false
(触发 TypeError
)。
让我们用它来验证新值
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // to intercept property writing
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // added successfully
numbers.push(2); // added successfully
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("This line is never reached (error in the line above)");
请注意:数组的内置功能仍然有效!值通过 push
添加。当添加值时,length
属性会自动增加。我们的代理不会破坏任何东西。
我们不必重写像 push
和 unshift
这样的值添加数组方法,等等,在其中添加检查,因为在内部它们使用 [[Set]]
操作,该操作被代理拦截。
所以代码干净简洁。
true
如上所述,有一些不变式需要保持。
对于 set
,它必须为成功写入返回 true
。
如果我们忘记这样做或返回任何假值,操作将触发 TypeError
。
使用“ownKeys”和“getOwnPropertyDescriptor”进行迭代
Object.keys
、for..in
循环和大多数其他遍历对象属性的方法使用 [[OwnPropertyKeys]]
内部方法(被 ownKeys
陷阱拦截)来获取属性列表。
这些方法在细节上有所不同
Object.getOwnPropertyNames(obj)
返回非符号键。Object.getOwnPropertySymbols(obj)
返回符号键。Object.keys/values()
返回具有enumerable
标志的非符号键/值(属性标志在文章 属性标志和描述符 中解释)。for..in
循环遍历具有enumerable
标志的非符号键,以及原型键。
…但它们都从该列表开始。
在下面的示例中,我们使用 ownKeys
陷阱来使 for..in
循环遍历 user
,以及 Object.keys
和 Object.values
,以跳过以下划线 _
开头的属性
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
到目前为止,它有效。
但是,如果我们返回一个对象中不存在的键,Object.keys
不会列出它
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
为什么?原因很简单:Object.keys
只返回具有 enumerable
标志的属性。为了检查它,它会为每个属性调用内部方法 [[GetOwnProperty]]
来获取 它的描述符。在这里,由于没有属性,它的描述符为空,没有 enumerable
标志,因此它被跳过。
为了让Object.keys
返回一个属性,我们需要它要么存在于对象中,并且具有enumerable
标志,要么我们可以拦截对[[GetOwnProperty]]
的调用(getOwnPropertyDescriptor
陷阱会这样做),并返回一个具有enumerable: true
的描述符。
以下是一个示例
let user = { };
user = new Proxy(user, {
ownKeys(target) { // called once to get a list of properties
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
再次注意:只有当属性不存在于对象中时,我们才需要拦截[[GetOwnProperty]]
。
使用“deleteProperty”和其他陷阱保护属性
有一个普遍的约定,即以下划线_
为前缀的属性和方法是内部的。不应该从对象外部访问它们。
虽然从技术上讲这是可能的
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
让我们使用代理来阻止对以_
开头的属性的任何访问。
我们需要以下陷阱
get
在读取此类属性时抛出错误,set
在写入时抛出错误,deleteProperty
在删除时抛出错误,ownKeys
从for..in
和Object.keys
等方法中排除以_
开头的属性。
以下是代码
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" doesn't allow to write _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" doesn't allow to delete _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" filters out _password
for(let key in user) alert(key); // name
请注意get
陷阱中(*)
行中的重要细节
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
为什么我们需要一个函数来调用value.bind(target)
?
原因是对象方法,例如user.checkPassword()
,必须能够访问_password
user = {
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
对user.checkPassword()
的调用将代理user
作为this
(点之前的对象变为this
),因此当它尝试访问this._password
时,get
陷阱会激活(它会在任何属性读取时触发)并抛出错误。
因此,我们在(*)
行中将对象方法的上下文绑定到原始对象target
。然后它们将来的调用将使用target
作为this
,而不会有任何陷阱。
这种解决方案通常有效,但并不理想,因为方法可能会将未代理的对象传递到其他地方,然后我们会变得混乱:原始对象在哪里,代理对象在哪里?
此外,一个对象可能会被多次代理(多个代理可能会对对象添加不同的“调整”),如果我们将未包装的对象传递给方法,可能会出现意外后果。
因此,这样的代理不应该在任何地方使用。
现代 JavaScript 引擎原生支持类中的私有属性,以 #
为前缀。它们在文章 私有和受保护的属性和方法 中有描述。不需要代理。
但是,此类属性也存在一些问题。特别是,它们不会被继承。
“在范围内”与“has”陷阱
让我们看更多例子。
我们有一个范围对象
let range = {
start: 1,
end: 10
};
我们想使用 in
运算符来检查一个数字是否在 range
中。
has
陷阱拦截 in
调用。
has(target, property)
target
– 是目标对象,作为第一个参数传递给new Proxy
,property
– 属性名称
这是演示
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
不错的语法糖,不是吗?而且实现起来非常简单。
包装函数:“apply”
我们也可以在函数周围包装一个代理。
apply(target, thisArg, args)
陷阱处理将代理作为函数调用
target
是目标对象(函数在 JavaScript 中是一个对象),thisArg
是this
的值。args
是一个参数列表。
例如,让我们回顾一下我们在文章 装饰器和转发,call/apply 中做的 delay(f, ms)
装饰器。
在那篇文章中,我们没有使用代理来实现它。对 delay(f, ms)
的调用返回一个函数,该函数在 ms
毫秒后将所有调用转发到 f
。
这是之前的基于函数的实现
function delay(f, ms) {
// return a wrapper that passes the call to f after the timeout
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// after this wrapping, calls to sayHi will be delayed for 3 seconds
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
正如我们已经看到的那样,这在大多数情况下都能正常工作。包装函数 (*)
在超时后执行调用。
但是包装函数不会转发属性读写操作或其他任何操作。包装后,对原始函数属性的访问将丢失,例如 name
、length
等。
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
Proxy
更加强大,因为它将所有内容转发到目标对象。
让我们使用 Proxy
代替包装函数
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)
结果是一样的,但现在不仅是调用,代理上的所有操作都将转发到原始函数。因此,在 (*)
行中包装后,sayHi.length
会正确返回。
我们得到了一个“更丰富”的包装器。
还存在其他陷阱:完整的列表在本文开头。它们的用法模式与上面类似。
Reflect
Reflect
是一个内置对象,它简化了创建 Proxy
的过程。
之前我们说过,内部方法,例如 [[Get]]
、[[Set]]
等,只是规范定义,不能直接调用。
Reflect
对象在一定程度上实现了这一点。它的方法是对内部方法的最小封装。
以下是一些操作和 Reflect
调用的示例,它们执行相同的操作
操作 | Reflect 调用 |
内部方法 |
---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
… | … | … |
例如
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
特别是,Reflect
允许我们将运算符(new
、delete
等)作为函数调用(Reflect.construct
、Reflect.deleteProperty
等)。这是一个有趣的功能,但还有一点很重要。
对于每个可以被 Proxy
拦截的内部方法,Reflect
中都有一个对应的方法,其名称和参数与 Proxy
拦截器相同。
因此,我们可以使用 Reflect
将操作转发到原始对象。
在这个例子中,get
和 set
拦截器都透明地(就像它们不存在一样)将读写操作转发到对象,并显示一条消息
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
这里
Reflect.get
读取对象属性。Reflect.set
写入对象属性,如果成功则返回true
,否则返回false
。
也就是说,一切都非常简单:如果拦截器想要将调用转发到对象,只需使用相同的参数调用 Reflect.<method>
即可。
在大多数情况下,我们可以在没有 Reflect
的情况下执行相同的操作,例如,读取属性 Reflect.get(target, prop, receiver)
可以用 target[prop]
替换。但是,有一些重要的细微差别。
代理 getter
让我们看一个例子,它演示了为什么 Reflect.get
更好。我们还将看到为什么 get/set
有第三个参数 receiver
,我们之前没有使用它。
我们有一个对象 user
,它具有 _name
属性和一个 getter。
这是一个围绕它的代理
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
get
拦截器在这里是“透明的”,它返回原始属性,不做任何其他操作。这对于我们的例子来说已经足够了。
一切似乎都很好。但是,让我们使示例稍微复杂一些。
从 user
继承另一个对象 admin
后,我们可以观察到不正确的行为
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
读取 admin.name
应该返回 "Admin"
,而不是 "Guest"
!
出了什么问题?也许我们在继承方面做错了什么?
但是,如果我们删除代理,那么一切将按预期工作。
问题实际上出在代理中,在代码行(*)
处。
-
当我们读取
admin.name
时,由于admin
对象没有该属性,所以会去它的原型链上查找。 -
原型是
userProxy
。 -
当从代理中读取
name
属性时,它的get
陷阱会被触发,并从原始对象中返回target[prop]
,如代码行(*)
所示。当
prop
是一个 getter 时,调用target[prop]
会以this=target
的上下文执行它的代码。因此,结果是来自原始对象target
的this._name
,也就是来自user
。
为了解决这种情况,我们需要receiver
,它是get
陷阱的第三个参数。它保存了要传递给 getter 的正确this
。在本例中,它是admin
。
如何为 getter 传递上下文?对于普通函数,我们可以使用call/apply
,但 getter 不是“调用”的,只是访问的。
Reflect.get
可以做到这一点。如果我们使用它,一切都会正常工作。
以下是修正后的版本
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
现在,receiver
保存了对正确this
(即admin
)的引用,它通过代码行(*)
中的Reflect.get
传递给 getter。
我们可以将陷阱重写得更简洁
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect
的调用方式与陷阱完全相同,并接受相同的参数。它们的设计初衷就是如此。
因此,return Reflect...
提供了一种安全且无需考虑的解决方案,可以转发操作并确保我们不会遗漏任何相关内容。
代理的局限性
代理提供了一种独特的方式,可以在最低级别更改或调整现有对象的行为。然而,它并不完美,也有一些局限性。
内置对象:内部槽
许多内置对象,例如Map
、Set
、Date
、Promise
等,都使用所谓的“内部槽”。
它们类似于属性,但保留用于内部、规范特定的目的。例如,Map
在内部槽[[MapData]]
中存储项目。内置方法直接访问它们,而不是通过[[Get]]/[[Set]]
内部方法。因此,Proxy
无法拦截它们。
为什么要关心?它们本来就是内部的!
问题在于,当这样的内置对象被代理后,代理就没有这些内部槽,因此内置方法会失败。
例如
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
在内部,Map
将所有数据存储在其 [[MapData]]
内部槽中。代理没有这样的槽。 内置方法 Map.prototype.set
方法尝试访问内部属性 this.[[MapData]]
,但由于 this=proxy
,无法在 proxy
中找到它,因此失败。
幸运的是,有一种方法可以解决它
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
现在它可以正常工作了,因为 get
陷阱将函数属性(例如 map.set
)绑定到目标对象 (map
) 本身。
与前面的示例不同,proxy.set(...)
内部 this
的值将不是 proxy
,而是原始的 map
。因此,当 set
的内部实现尝试访问 this.[[MapData]]
内部槽时,它会成功。
Array
没有内部槽一个值得注意的例外:内置 Array
不使用内部槽。这是出于历史原因,因为它出现的时间太久了。
因此,在代理数组时不存在此类问题。
私有字段
私有类字段也会发生类似的事情。
例如,getName()
方法访问私有 #name
属性,并在代理后中断
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
原因是私有字段使用内部槽实现。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]
。
在调用 getName()
中,this
的值为代理的 user
,它没有包含私有字段的槽。
再次,使用绑定方法的解决方案使其有效
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
也就是说,该解决方案有缺点,如前所述:它将原始对象暴露给方法,可能允许它被进一步传递并破坏其他代理功能。
代理 != 目标
代理和原始对象是不同的对象。这是自然的,对吧?
因此,如果我们使用原始对象作为键,然后代理它,那么代理将找不到
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
正如我们所见,在代理后,我们无法在集合 allUsers
中找到 user
,因为代理是不同的对象。
===
代理可以拦截许多运算符,例如 new
(使用 construct
)、in
(使用 has
)、delete
(使用 deleteProperty
)等等。
但是,无法拦截对象的严格相等测试。一个对象仅与其自身严格相等,而与其他任何值都不相等。
因此,所有比较对象是否相等的运算符和内置类都将区分对象和代理。这里没有透明的替换。
可撤销代理
可撤销代理是可以禁用的代理。
假设我们有一个资源,并且希望随时关闭对它的访问。
我们可以做的是将其包装在一个可撤销的代理中,没有任何陷阱。这样的代理将把操作转发到对象,并且我们可以随时禁用它。
语法是
let {proxy, revoke} = Proxy.revocable(target, handler)
该调用返回一个包含 proxy
和 revoke
函数的对象,用于禁用它。
以下是一个示例
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// pass the proxy somewhere instead of object...
alert(proxy.data); // Valuable data
// later in our code
revoke();
// the proxy isn't working any more (revoked)
alert(proxy.data); // Error
调用revoke()
会从代理中移除对目标对象的所有内部引用,因此它们不再连接。
最初,revoke
与proxy
是分开的,这样我们就可以在当前作用域中保留revoke
的同时传递proxy
。
我们也可以通过设置proxy.revoke = revoke
将revoke
方法绑定到代理。
另一种选择是创建一个WeakMap
,它以proxy
作为键,相应的revoke
作为值,这样可以方便地找到代理的revoke
。
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..somewhere else in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error (revoked)
我们在这里使用WeakMap
而不是Map
,因为它不会阻止垃圾回收。如果代理对象变得“不可达”(例如,不再有变量引用它),WeakMap
允许它与我们不再需要的revoke
一起从内存中清除。
参考资料
总结
Proxy
是一个对象包装器,它将对它的操作转发到对象,并可以选择性地拦截其中一些操作。
它可以包装任何类型的对象,包括类和函数。
语法是
let proxy = new Proxy(target, {
/* traps */
});
…然后我们应该在任何地方使用proxy
而不是target
。代理没有自己的属性或方法。如果提供了陷阱,它会拦截操作,否则会将其转发到target
对象。
我们可以拦截
- 读取(
get
)、写入(set
)、删除(deleteProperty
)属性(即使是非存在的属性)。 - 调用函数(
apply
陷阱)。 new
运算符(construct
陷阱)。- 许多其他操作(完整列表在文章开头和文档中)。
这使我们能够创建“虚拟”属性和方法,实现默认值、可观察对象、函数装饰器等等。
我们还可以用不同的代理多次包装一个对象,用各种功能方面装饰它。
Reflect API 旨在补充Proxy。对于任何Proxy
陷阱,都有一个具有相同参数的Reflect
调用。我们应该使用它们将调用转发到目标对象。
代理有一些限制
- 内置对象具有“内部槽”,对它们的访问无法被代理。请参阅上面的解决方法。
- 私有类字段也是如此,因为它们在内部使用槽实现。因此,代理方法调用必须将目标对象作为
this
来访问它们。 - 对象相等性测试
===
无法被拦截。 - 性能:基准测试取决于引擎,但通常使用最简单的代理访问属性要慢几倍。在实践中,这只会影响一些“瓶颈”对象。
评论
<code>
标签,对于多行代码,请将其包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)