本文将着重于介绍 Proxy 强大的功能,如果您需要了解 Proxy 的基本定义和语法,请参考 Proxy
什么是 Proxy ?它到底起什么作用?在解释之前,让我们看一个真实的例子。
我们每个人在日常生活中都有很多事情要做,比如阅读电子邮件、接收快递等等。有时我们可能会感到有点焦虑:我们的邮件列表上有很多垃圾邮件,需要花费很多时间筛选;收到的货物中可能含有恐怖分子安放的炸弹,威胁我们的安全。
这时你可能需要一个忠诚的管家。你希望管家帮你做以下事情:让它检查你的收件箱,在你开始阅读之前删除所有垃圾邮件;当你收到包裹时,让它用专业设备检查包裹,确保里面没有炸弹。
在上面的例子中,管家就是我们的 Proxy。当我们想做一些事情的时候,管家为我们做了一些额外的事情。
现在让我们回到 JavaScript。我们知道 JavaScript 是一种面向对象的编程语言,没有对象我们就无法编写代码。但是 JavaScript 对象总是一丝不挂运行的,你可以用它们做任何事情。这在很多时候,会降低代码的安全性。
在 ECMAScript2015 中引入了 Proxy。有了 Proxy,我们可以找到一个忠实的管家,帮助我们增强对象的原有功能。
使用 Proxy 的基本语法如下所示:
// 这是一个一般的对象
let obj = {a: 1, b:2}
// 使用 Proxy 配置对象
let objProxy = new Proxy(obj, handler)
这只是一个伪代码。因为我们还没有编写 handler,所以这段代码暂时不能正常运行。
对一个人来说,我们可能会有阅读邮件、取快递等操作,管家可以帮助我们做这些。对于对象来说,它可以读取属性、设置属性等,这些功能可以通过 Proxy 对象进行增强。
在 handler 中,我们可以列出要 Proxy 的操作。例如,如果要在获取对象属性时在控制台中打印语句,可以这样写:
let obj = {a: 1, b:2}
// 使用 Proxy 语法为对象找到一个管家
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[property]
}
})
在上面的例子中,我们的 handler 是:
{
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[propery]
}
}
当我们试图读取对象的属性时,get 函数将执行:
get 函数可以有 3 个参数:
item
: 对象自身property
: 你想要读取属性的属性名itemProxy
: 我们刚刚创建的管家对象
你可能已经在其他地方阅读过 Proxy 的教程,然后你会注意到我对参数的命名与它们不同。我这样做是为了更接近我前面的示例,以帮助你理解。希望对你有用。
那么 get 函数的返回值就是读取属性的值。因为我们还不想更改任何内容,所以只返回原始对象的属性值。
如果有必要,我们也可以改变结果。例如,我们可以这样做:
let obj = {a: 1, b:2}
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[property] * 2
}
})
以下是读取其属性的结果:
我们将通过实际例子来说明这一技巧的实际应用。
除了拦截对属性的读取之外,我们还可以拦截对属性的修改。比如这样:
let obj = {a: 1, b:2}
let objProxy = new Proxy(obj, {
set: function(item, property, value, itemProxy){
console.log(`You are setting '${value}' to '${property}' property`)
item[property] = value
}
})
当我们试图设置对象属性的值时,set 函数被触发。
因为在设置属性值时需要传递一个额外的值,所以上面的 set 函数比 get 函数多接受一个参数。
除了拦截对属性的读取和修改之外,Proxy 还可以拦截对象的总共13个操作。
它们是:
- get(item, propKey, itemProxy): 拦截对象属性的读取操作,比如
obj.a
和obj['b']
- set(item, propKey, value, itemProxy): 拦截对象属性的设置操作,比如
obj.a = 1
- has(item, propKey): 拦截操作
propKey in objProxy
, 返回一个布尔值 - deleteProperty(item, propKey): 拦截操作
delete proxy[propKey]
, 返回一个布尔值 - ownKeys(item): 拦截操作,比如
Object.getOwnPropertyNames(proxy)
,Object.getOwnPropertySymbols(proxy)
,Object.keys(proxy)
,for...in
, 返回一个数组。该方法返回目标对象自身属性的属性名, 然而Object.keys()
操作返回的结果是只包含目标对象自身的可枚举属性 - getOwnPropertyDescriptor(item, propKey): 拦截操作
Object.getOwnPropertyDescriptor(proxy, propKey)
, 返回属性的描述符 - defineProperty(item, propKey, propDesc): 拦截操作:
Object.defineProperty(proxy, propKey, propDesc)
,Object.defineProperties(proxy, propDescs)
, 返回一个布尔值 - preventExtensions(item): 拦截操作
Object.preventExtensions(proxy)
, 返回一个布尔值 - getPrototypeOf(item): 拦截操作
Object.getPrototypeOf(proxy)
,返回一个对象 - isExtensible(item): 拦截操作
Object.isExtensible(proxy)
,返回一个布尔值 - setPrototypeOf(item, proto): 拦截操作
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值
如果目标对象是一个函数,则有两个附加的操作要拦截
- apply(item, object, args): 拦截函数调用操作,比如
proxy(...args)
,proxy.call(object, ...args)
,proxy.apply(...)
- construct(item, args): 拦截 Proxy 实例作为构造函数来调用的操作,例如
new proxy(...args)
有些拦截不常用,所以我就不详细介绍了。现在让我们进入真实的例子,看看 Proxy 实际上能为我们做些什么。
1、实现数组的负索引
我们知道其他一些编程语言,比如 Python,支持访问数组的负索引。
负索引以数组的最后一个位置为起点,向前计数。例如:
- arr[-1] 是数组的最后一个元素。
- arr[-3] 是数组的倒数第 3 个元素。
很多人认为这是一个非常有用的特性,但是不幸的是,JavaScript 目前不支持负索引语法。
但是 JavaScript 中强大的 Proxy 为我们提供了元编程的能力。
我们可以将数组包装为 Proxy 对象。当用户试图访问负索引时,我们可以通过 Proxy 的 get 方法拦截此操作。然后根据前面定义的规则将负索引转换为正索引,访问就完成了。
让我们从一个基本操作开始:拦截数组属性的读取。
function negativeArray(array) {
return new Proxy(array, {
get: function(item, propKey){
console.log(propKey)
return item[propKey]
}
})
}
上面的函数可以包装一个数组,让我们看看它是如何使用的。
正如你所看到的,我们对数组属性的读取确实被拦截了。
请注意:JavaScript 中的对象只能具有 String 或 Symbol 类型的键。当我们编写 arr[1] 时,它实际上正在访问 arr['1']
。关键是字符串 “1”
,而不是数字 1
。
所以现在我们需要做的是:当用户试图访问一个属性,这个属性它是数组的索引,并且发现它是一个负索引,然后拦截并做相应的处理;如果这个属性不是索引,或者是一个正的索引,我们什么都不做。
结合以上需求,我们可以编写以下伪代码。
function negativeArray(array) {
return new Proxy(array, {
get: function(target, propKey){
if(/** 属性是负索引 */){
// 将负索引转换成正索引
}
return target[propKey]
})
}
那么我们如何识别负索引呢?这里很容易犯错误,所以我要详细介绍一下。
首先,Proxy 的 get 方法将拦截对数组所有属性的访问,包括对数组索引的访问和对数组其他属性的访问。只有属性名可以转换为整数时,才执行访问数组中元素的操作。我们实际上需要拦截这个操作来访问数组中的元素。
我们可以通过检查数组的属性是否可以转换为整数来确定它是否是索引。
Number(propKey) != NaN && Number.isInteger(Number(propKey))
所以,完整的代码可以这样写:
function negativeArray(array) {
return new Proxy(array, {
get: function(target, propKey){
if (Number(propKey) != NaN && Number.isInteger(Number(propKey)) && Number(propKey) < 0) {
propKey = String(target.length + Number(propKey));
}
return target[propKey]
}
})
}
下面是一个例子:
2、数据校验
众所周知,Javascript 是一种弱类型语言。通常,当一个对象被创建时,它是一丝不挂运行的。任何人都可以修改它。
但大多数情况下,一个对象的属性值需要满足某些条件。例如,记录用户信息的对象的年龄属性,它的值通常应该是一个大于 0、小于 150 的整数。
let person1 = {
name: 'Jon',
age: 23
}
但是,默认情况下,JavaScript 没有提供安全机制,你可以随意更改此值。
person1.age = 9999
person1.age = 'hello world'
为了使代码更安全,我们可以用 Proxy 来包装对象。我们可以拦截对象的 set 操作,并验证 age 字段的新值是否符合规则。
let ageValidate = {
set (item, property, value) {
if (property === 'age') {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new TypeError('age should be an integer between 0 and 150');
}
}
item[property] = value
}
}
现在我们尝试修改这个属性的值,我们可以看到我们设置的保护机制正在工作。
3、关联属性
很多时候,一个对象的属性是相互关联的。例如,对于存储用户信息的对象,其邮政编码和位置是两个高度相关的属性。当用户的邮政编码被确定时,他的位置也被确定。
为了方便来自不同国家的读者理解,我在这里使用了一个虚拟的示例。假设位置和邮政编码具有以下关系:
JavaScript Street -- 232200
Python Street -- 234422
Golang Street -- 231142
以下是表示它们对应关系的代码:
const location2postcode = {
'JavaScript Street': 232200,
'Python Street': 234422,
'Golang Street': 231142
}
const postcode2location = {
'232200': 'JavaScript Street',
'234422': 'Python Street',
'231142': 'Golang Street'
}
然后请看下面的例子:
let person = {
name: 'Jon'
}
person.postcode = 232200
我们希望当我们设置 person.postcode=232200
时自动触发 person.location='JavaScript Street'
。
解决办法如下:
let postcodeValidate = {
set(item, property, value) {
if(property === 'location') {
item.postcode = location2postcode[value]
}
if(property === 'postcode'){
item.location = postcode2location[value]
}
}
}
所以我们把邮政编码和位置绑在一起了。
4、私有属性
我们知道 JavaScript 一直以来都不支持私有属,这使得我们在编写代码时无法合理地管理访问权限。
为了解决这个问题,JavaScript 社区的惯例是将以 _
字符开头的属性视为私有属性。
var obj = {
a: 1,
_value: 22
}
上面的 _value
属性被认为是私有的。但是,必须指出的是,这只是一种惯例,在语言层面上并没有这样的规则。
我们现在有了 Proxy, 可以模拟私有属性了。
与一般的属性相比,私有属性具有以下特点:
- 属性值不能被读取
- 当用户想要查看对象的所有属性名时,这个属性是不可见的
然后,我们可以检查一下前面提到的 Proxy 的 13 个拦截操作,可以看到有 3 个操作需要被拦截。
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
// 拦截操作 `propKey in objProxy`
has: (obj, prop) => {},
// 拦截操作,比如 `Object.keys(proxy)`
ownKeys: obj => {},
// 拦截读取对象的属性操作
get: (obj, prop, rec) => {})
});
}
然后,我们添加适当的代码到上面的伪代码中:如果发现用户试图访问以 _
开头的字段,则拒绝访问。
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
has: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return false
}
return prop in obj
},
ownKeys: obj => {
return Reflect.ownKeys(obj).filter(
prop => typeof prop !== "string" || !prop.startsWith(prefix)
)
},
get: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return undefined
}
return obj[prop]
}
});
}
下面是一个例子: