代理与反射(二)


代理与反射(二)

使用代理模式可以实现一些有用的功能。

捕获操作

通过添加对应捕获器,就可以捕获getsethas等操作,可以监控这个对象何时在何处被访问过,并且能在访问、修改前干想干的事,并通过反射重新实现原功能。

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

image-20220503205936504

这里有一个需要小小注意的点:通过代理对象的操作才会被捕获,而直接操作目标对象的操作不会被捕获。

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)

image-20220503205922840

隐藏属性

因为代理的内部实现对于外部的代码来说是不可见的,所以想要隐藏目标对象上的属性也很容易实现。我们上面说过,需要通过反射来实现原功能,但是我们也可以不实现原功能,而是返回其他值。

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') {
// 隐藏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)

image-20220503210140040

从上图,我们可以知道,直接访问目标对象、目标对象属性以及访问代理对象都能能到一样的结果。但是,通过代理访问 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);

image-20220503211445808

当我们返回 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);
}


// 在`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捕获器来实现代理。

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)

image-20220503234749114

我们可以发现,第一次是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)
// Reflect.set返回boolean值,该值指示该属性是否已成功设置

if (result) {
// 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去

emit(Reflect.get(target, property, receiver))
}

return result
}
})

proxy.push(111)
proxy.push(222)
console.log(proxy)

image-20220503235521715

这里可以发现,我们 push两条数据,但是会触发事件分发程序4次,这是为什么?

这就是这一小节上面讲的,push实际上是分成两个阶段的,第一轮先修改数组,第二轮再修改数组长度

所以,我们不应该只是判断是否被成功设置,还应该判断属性是不是 length

1
2
3
4
5
if (result && property !== 'length') {
// 如果被成功设置了,调用事件分派函数,把新插入的值作为参数传过去

emit(Reflect.get(target, property, receiver))
}

image-20220504000010868


文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录