代理与反射(二)
使用代理模式可以实现一些有用的功能。
捕获操作
通过添加对应捕获器,就可以捕获get
、set
、has
等操作,可以监控这个对象何时在何处被访问过,并且能在访问、修改前干想干的事,并通过反射重新实现原功能。
const user = {
name: 'clz'
}
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`访问 ${property}`)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
console.log(`设置 ${property}=${value}`)
return Reflect.set(...arguments)
}
})
console.log(proxy.name)
proxy.age = 21
这里有一个需要小小注意的点:通过代理对象的操作才会被捕获,而直接操作目标对象的操作不会被捕获。
const user = {
name: 'clz'
}
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`访问 ${property}`)
return Reflect.get(...arguments)
}
})
console.log(proxy.name)
console.log(user.name)
隐藏属性
因为代理的内部实现对于外部的代码来说是不可见的,所以想要隐藏目标对象上的属性也很容易实现。我们上面说过,需要通过反射来实现原功能,但是我们也可以不实现原功能,而是返回其他值。
const user = {
name: 'clz',
age: 21
};
const proxy = new Proxy(user, {
get(target, property, receiver) {
if (property === 'name') {
// 隐藏name属性
return undefined;
}
return Reflect.get(...arguments)
}
});
console.log(user)
console.log(user.name)
console.log(user.age)
console.log('%c%s', 'font-size:24px;color:red', '=================')
console.log(proxy)
console.log(proxy.name)
console.log(proxy.age)
从上图,我们可以知道,直接访问目标对象、目标对象属性以及访问代理对象都能能到一样的结果。但是,通过代理访问 name
属性会得到 undefined
,因为我们在捕获操作中进行了隐藏属性。
验证
属性验证
因为所有的赋值操作都会触发set
捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值,不通过验证,直接return false
即可拒绝赋值。
const user = {
name: 'clz',
age: 21
};
const proxy = new Proxy(user, {
set(target, property, value) {
if (typeof value !== 'number') {
console.log('不是number类型,拒绝赋值')
return false;
} else {
return Reflect.set(...arguments);
}
}
});
proxy.age = 999;
console.log(proxy.age);
proxy.age = '111';
console.log(proxy.age);
当我们返回 false
时,即不通过验证,就可以不进行原始行为的实现,
函数参数验证
和验证对象属性类似,也可以对函数的参数进行审查。
首先,函数也是能使用代理的。apply
捕获器会在调用函数时被调用。
function fn() { }
const proxy = new Proxy(fn, {
apply() {
console.log('调用函数')
}
});
proxy(1, 2, 3, 4, 5)
所以我们应该在 apply
捕获器中进行操作验证。
function mysort(...nums) {
return nums.sort((a, b) => a - b);
}
// 在`apply`捕获器中进行参数验证
const proxy = new Proxy(mysort, {
apply(target, thisArg, argumentsList) {
// target: 目标对象
// thisArg: 调用函数时的this参数
// argumentsList: 调用函数时的参数列表
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw '函数参数必须为number';
}
}
return Reflect.apply(...arguments);
}
});
let fin = proxy(2, 1, 6, 3, 4, 3);
console.log(fin);
fin = proxy(2, 1, 'hello', 3, 4, 3);
console.log(fin);
构造函数同理,只是构造函数是通过constructor
捕获器来实现代理。
function Person(name) {
this.name = name
}
const proxy = new Proxy(Person, {
construct() {
console.log(123)
return Reflect.construct(...arguments)
}
});
const p = new proxy('clz')
console.log(p)
数据绑定
通过代理可以把原本运行中不相关的部分联系到一起。
例子:将被代理的类绑定到一个全局的实例集合,将所有创建的实例都添加到这个集合中。`
const userList = []
class User {
constructor(name) {
this.name_ = name;
}
}
const proxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments)
userList.push(newUser)
return newUser
}
})
new proxy('clz')
new proxy('czh')
console.log(userList)
事件分发程序
在开始之前,先来一个小问题。
const nums = []
const proxy = new Proxy(nums, {
set(target, property, value, receiver) {
console.log('setting')
return Reflect.set(...arguments)
}
})
proxy.push(1)
上面的代码会打印两次setting
。
这是为什么呢?
让我们把它每一轮修改的属性打印出来,研究一下。
const nums = []
const proxy = new Proxy(nums, {
set(target, property, value, receiver) {
console.log('setting')
console.log(property)
return Reflect.set(...arguments)
}
})
proxy.push(1)
我们可以发现,第一次是0,第二次是 length
。也就是说,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度,所以会打印两轮。(没有找到权威的解释,实践测试得出的结论,有问题可评论)
回到正题:可以把集合绑定给一个事件分发程序,实现每次插入新实例时,都会发送消息。
const nums = []
function emit(newValue) {
console.log('有新数据,新数据是', newValue)
}
const proxy = new Proxy(nums, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments)
// Reflect.set返回boolean值,该值指示该属性是否已成功设置
if (result) {
// 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去
emit(Reflect.get(target, property, receiver))
}
return result
}
})
proxy.push(111)
proxy.push(222)
console.log(proxy)
这里可以发现,我们 push
两条数据,但是会触发事件分发程序4次,这是为什么?
这就是这一小节上面讲的,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度。
所以,我们不应该只是判断是否被成功设置,还应该判断属性是不是 length
。
if (result && property !== 'length') {
// 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去
emit(Reflect.get(target, property, receiver))
}