代理与反射(二)
使用代理模式可以实现一些有用的功能。
捕获操作
通过添加对应捕获器,就可以捕获get
、set
、has
等操作,可以监控这个对象何时在何处被访问过,并且能在访问、修改前干想干的事,并通过反射重新实现原功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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
|
这里有一个需要小小注意的点:通过代理对象的操作才会被捕获,而直接操作目标对象的操作不会被捕获。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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)
|
隐藏属性
因为代理的内部实现对于外部的代码来说是不可见的,所以想要隐藏目标对象上的属性也很容易实现。我们上面说过,需要通过反射来实现原功能,但是我们也可以不实现原功能,而是返回其他值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const user = { name: 'clz', age: 21 };
const proxy = new Proxy(user, { get(target, property, receiver) { if (property === '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
即可拒绝赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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
捕获器会在调用函数时被调用。
1 2 3 4 5 6 7 8 9 10
| function fn() { }
const proxy = new Proxy(fn, { apply() { console.log('调用函数') } });
proxy(1, 2, 3, 4, 5)
|
所以我们应该在 apply
捕获器中进行操作验证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function mysort(...nums) { return nums.sort((a, b) => a - b); }
const proxy = new Proxy(mysort, { apply(target, thisArg, 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
捕获器来实现代理。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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)
|
数据绑定
通过代理可以把原本运行中不相关的部分联系到一起。
例子:将被代理的类绑定到一个全局的实例集合,将所有创建的实例都添加到这个集合中。`
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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)
|
事件分发程序
在开始之前,先来一个小问题。
1 2 3 4 5 6 7 8 9 10 11
| const nums = []
const proxy = new Proxy(nums, { set(target, property, value, receiver) { console.log('setting')
return Reflect.set(...arguments) } })
proxy.push(1)
|
上面的代码会打印两次setting
。
这是为什么呢?
让我们把它每一轮修改的属性打印出来,研究一下。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度,所以会打印两轮。(没有找到权威的解释,实践测试得出的结论,有问题可评论)
回到正题:可以把集合绑定给一个事件分发程序,实现每次插入新实例时,都会发送消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const nums = []
function emit(newValue) { console.log('有新数据,新数据是', newValue) }
const proxy = new Proxy(nums, { set(target, property, value, receiver) { const result = Reflect.set(...arguments)
if (result) {
emit(Reflect.get(target, property, receiver)) }
return result } })
proxy.push(111) proxy.push(222) console.log(proxy)
|
这里可以发现,我们 push
两条数据,但是会触发事件分发程序4次,这是为什么?
这就是这一小节上面讲的,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度。
所以,我们不应该只是判断是否被成功设置,还应该判断属性是不是 length
。
1 2 3 4 5
| if (result && property !== 'length') {
emit(Reflect.get(target, property, receiver)) }
|