深入了解 Proxy 代理
共 13478字,需浏览 27分钟
·
2021-03-17 02:35
代理对象封装另一个对象并拦截操作,如读取/写入属性和其他操作,可以选择自己处理它们,或透明地允许对象处理它们。
很多库和一些浏览器框架都使用代理。在本文中,我们将看到许多实际应用程序。
Proxy
语法如下:
let proxy = new Proxy(target, handler)
target - 是一个要包装的对象,可以是任何东西,包括函数。
handler - 代理配置:一个带有“陷阱”的对象,拦截操作的方法。-例如,读取target属性时设置trap,写入target属性时设置trap,等等。
对于代理上的操作,如果handler中有相应的陷阱,那么它就会运行,并且代理有机会处理它,否则操作就会在目标上执行。
作为一个开始的例子,让我们创建一个没有任何陷阱的代理:
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.test=target上的值。
读取操作 proxy.test 从 target 返回值。
迭代代理返回目标值。
正如我们所见,没有任何陷阱,proxy是一个透明的目标包装器。
Proxy是一种特殊的“外来对象”。它没有自己的属性。使用空处理程序,它透明地将操作转发给target。
为了激活更多的功能,让我们添加陷阱。
我们能用他们拦截什么?
对于对象上的大多数操作,JavaScript规范中都有一个所谓的“内部方法”,它描述了它在最低级别的工作方式。例如[[Get]],读取属性的内部方法,[[Set]],写入属性的内部方法,等等。这些方法仅在规范中使用,我们不能直接通过名称调用它们。
代理陷阱拦截这些方法的调用。它们在代理规范和下表中列出。
对于每个内部方法,在该表中都有一个陷阱:我们可以添加到新代理的handler参数的方法名来拦截操作:
使用 get 方式获取默认值
最常见的陷阱是用于读/写属性的。
为了拦截读取,处理程序应该有一个方法get(目标、属性、接收器)。
当一个属性被读取时,它会触发,参数如下:
target—是目标对象,作为第一个参数传递给新代理,
property -属性名称,
receiver——如果目标属性是一个getter,那么receiver就是将在其调用中使用的对象。通常这是代理对象本身(或者从它继承的对象,如果我们从代理继承的话)。现在我们不需要这个论证,所以后面会更详细地解释。
让我们使用get来实现对象的默认值。
我们将创建一个数字数组,对于不存在的值返回0。
通常,当一个人试图获取一个不存在的数组项时,他们得到的是未定义的,但是我们将把一个常规的数组包装到代理中,以捕获读取,如果没有这样的属性则返回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)
正如我们所见,诱捕陷阱很容易做到。
我们可以使用代理来实现“默认”值的任何逻辑。
想象一下我们有一本词典,里面有一些短语和它们的翻译:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // 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)
使用 set 验证
假设我们想要一个专门用于数字的数组。如果添加了另一种类型的值,应该会出现错误。
set trap在写入属性时触发。
set(target, property, value, receiver)
target—是目标对象,作为第一个参数传递给新代理,
property -属性名称,
value -属性值,
receiver——与get trap类似,只对setter属性有效。
如果设置成功,set trap应该返回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]]操作。
因此,代码是干净和简洁的。
使用 ownKeys, getOwnPropertyDescriptor 进行迭代
Object.keys, for...in 和迭代对象属性的大多数其他方法使用[[OwnPropertyKeys]]内部方法(被ownKeys陷阱截获)来获得属性列表。
这些方法在细节上有所不同:
Object.getOwnPropertyNames(obj) 返回非符号键。
Object.getOwnPropertySymbols(obj) 返回符号键。
Object.keys/values()返回带有可枚举标志的非符号键/值(属性标志在“属性标志和描述符”一文中解释过)。
for..in 循环遍历带有enumerable标志的非符号键和原型键。
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
标志,或者可以拦截对[[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
使用 deleteProperty 保护属性
有一个广泛的约定,即以下划线为前缀的属性和方法是内部的。它们不应该从对象外部访问。
从技术上讲,这是可能的:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
让我们使用代理来防止任何以_开头的属性的访问。
我们需要陷阱:
读取这样的属性时抛出错误,
设置为写入时抛出错误,
删除时抛出错误,
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
:
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
使用 has in range
let range = {
start: 1,
end: 10
};
我们想使用in操作符来检查一个数字是否在范围内。
has陷阱在调用中拦截。
has(target, property)
target — 是目标对象,作为第一个参数传递给新代理,
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
是目标对象(function
是JavaScript
中的对象),thisArg
是this
的值。args
是一个参数列表。
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)
正如我们已经看到的,这基本上是可行的。包装器函数(*)在超时后执行调用。
但是包装器函数不转发属性读/写操作或其他任何操作。包装后,对原始函数的属性的访问将丢失,例如名称、长度等:
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)
理要强大得多,因为它将所有内容转发给目标对象。
让我们使用代理代替包装函数:
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)