第9章 代理与反射
# 代理基础
# 创建空代理
代理是使用 Proxy
构造函数创建的。这个构造函数接收2个参数:目标对象和处理程序对象,均为必传。
const target = {
id:'hello'
}
const handler = {}
const proxy = new Proxy(target,handler)
console.log(proxy.id) // hello
console.log(target.id) // hello
// 结论1:id属性会访问同一个值
target.id = 'foo'
console.log(target.id) // foo
console.log(proxy.id) // foo
// 结论2:给目标属性赋值会反映到两个对象上,因为它们访问的是同一个值
proxy.id = 'bar'
console.log(target.id) // bar
console.log(proxy.id) // bar
// 结论3:给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
console.log(target.hasOwnProperty('id')) // true
console.log(proxy.hasOwnProperty('id')) // true
// 结论4:hasOwnProperty()方法都会应用到目标对象
try{
console.log(target instanceof Proxy)
}catch(e){
console.log('target-error',e) // target-error TypeError: Function has non-object prototype 'undefined' in instanceof check
}
try{
console.log(proxy instanceof Proxy)
}catch(e){
console.log('proxy-error',e) // proxy-error TypeError: Function has non-object prototype 'undefined' in instanceof check
}
// 结论5:不能使用instanceof操作符
console.log(target===proxy) // false
// 结论6:严格相等可以区分代理和目标
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
28
29
30
31
32
33
34
35
36
37
38
39
40
# 定义捕获器
捕获器(trap)是从操作系统中借用的概念,可以理解为拦截器,在js框架Vue中使用其特点来进行属性劫持。
捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = {
foo:'bar'
}
const handler = {
get(){
return 'handler override'
}
}
const proxy = new Proxy(target,handler)
console.log(target.foo) // bar
console.log(proxy.foo) // handler override
// 结论:在代理对象上执行操作才会触发捕获器,目标对象并不会触发
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 捕获器参数和反射API
const target = {
foo:'bar'
}
const handler ={
get(trapTarget,property,receiver){
console.log(trapTarget===target) // true
console.log(property) // foo
console.log(receiver===proxy) // true
}
}
const proxy = new Proxy(target,handler)
proxy.foo
// 结论:get捕获器有3个参数,分别是目标对象、要查询的属性和代理对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这些参数可以重建被捕获方法的原始行为。
// ...
get(trapTarget,property,receiver){
return trapTarget[property]
}
// ...
console.log(proxy.foo) // bar
2
3
4
5
6
实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局Reflect
对象上的 同名方法来轻松重建。
处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法。比如:
// ...
// return trapTarget[property]
return Reflect.get(...arguments)
// ...
2
3
4
甚至更简洁
// ...
const handler ={
get: Reflect.get
}
// ...
2
3
4
5
完整的例子
const target = {
foo:'bar',
aaa:'bbb'
}
const handler ={
get(){
const decoration = '!!!'
return Reflect.get(...arguments) + decoration
}
}
const proxy = new Proxy(target,handler)
console.log(proxy.foo) // bar!!!
console.log(proxy.aaa) // bbb!!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 可撤销代理
revocable
方法支持撤销代理对象与目标对象的关联。撤销行为不可逆,撤销函数是幂等的。
const target = {
foo:'bar'
}
const handler = {
get(){
return 'hello'
}
}
const {proxy,revoke} = Proxy.revocable(target,handler)
console.log(target.foo) // bar
console.log(proxy.foo) // hello
revoke()
console.log(proxy.foo) // TypeError
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 实用反射API
注意:
- 反射API并不限于捕获处理程序
- 大多数反射API在Object类型上有对应的方法
Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
状态标记
下面通过2段代码进行说明,反射方法返回的“状态标记”有何作用。
const o = {}
try {
Object.defineProperties(o, 'foo', 'bar')
console.log('success')
} catch (e) {
console.log('fail')
}
// 重构后
const o = {}
const isSuccess = Reflect.defineProperty(o, 'foo', { value: 'bar' })
if (isSuccess) {
console.log('success')
} else {
console.log('fail')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
替代操作符
Reflect.get():可以替代对象属性访问操作符。
Reflect.set():可以替代=赋值操作符。
Reflect.has():可以替代in操作符
Reflect.deletProperty():可以替代delete操作符。
Reflect.construct():可以替代new操作符。
# 代理的问题与不足
代理的this
代理与内部槽位
比如Date内置对象,代理对象没有内部槽位[[NumberDate]],导致无法访问Date对象的方法。
# 代理捕获器与反射方法
代理可以捕获13种不同的基本操作。只要在代理上调用,所有捕获器都会拦截他们对应的反射API操作。
# get()
get()捕获器在代理对象获取属性值的操作中被调用。对应的反射API方法是Reflect.get()
。
const target = {
foo: 1,
}
const proxy = new Proxy(target, {
get(target, property, receiver) {
console.log('getter') // getter
return Reflect.get(...arguments)
},
})
console.log(proxy.foo) // 1
2
3
4
5
6
7
8
9
10
11
12
# set()
set()捕获器在设置属性值的操作中被调用。对应的反射API方法是Reflect.set()
。
const target = {
foo: 1,
}
const proxy = new Proxy(target, {
set(target, property, value, receiver) {
console.log('setter')
return Reflect.set(...arguments)
},
})
proxy.foo = 2 // setter
console.log(proxy.foo) // 2
2
3
4
5
6
7
8
9
10
11
12
13
# 其他捕获器
其他11种捕获器如下
捕获器 | 什么操作符中调用 | 对应反射API方法 |
---|---|---|
has() | in | Reflect.has() |
defineProperty() | Object.defineProperty() | Reflect.defineProperty() |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
deleteProperty() | delete | Reflect.deleteProperty() |
ownKeys() | Object.keys() | Reflect.ownKeys() |
getPrototypeof() | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf() | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible() | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions() | Object.preventExtensions() | Reflect.preventExtensions() |
apply() | 调用函数时 | Reflect.apply() |
construct() | new | Reflect.construct() |
# 代理模式
使用代理可以在代码中实现一些有用的编程模式。(注:这些模式也可以不使用Proxy来实现)
# 跟踪属性访问
通过捕获器如get、set和has,可以知道对象属性什么时候被访问、被查询。
示例见上述get()和set()。
# 隐藏属性
代理的内部实现对外部代码是不可见的,因此很容易隐藏目标对象上的属性。如果不使用Proxy,我们也可以通过闭包的方式来实现(利用作用域访问规则)。
const hiddenProperties = ['foo', 'bar']
const targetObject = {
foo: 1,
bar: 2,
baz: 3,
}
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined
} else {
return Reflect.get(...arguments)
}
},
set(target, property, value) {
if (hiddenProperties.includes(property)) {
return false
} else {
return Reflect.set(...arguments)
}
},
})
console.log(proxy.foo) // undefined
console.log(proxy.baz) // 3
console.log(targetObject.foo) // 1
console.log(targetObject.baz) // 3
proxy.foo = 10
proxy.baz = 30
console.log(proxy.foo) // undefined
console.log(proxy.baz) // 30
console.log(targetObject.foo) // 1
console.log(targetObject.baz) // 30
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
28
29
30
31
32
33
34
35
36
37
38
39
# 属性验证
const target = {
onlyNumbersGoHere: 0,
}
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== 'number') {
return false
} else {
return Reflect.set(...arguments)
}
},
})
proxy.onlyNumbersGoHere = 1
console.log(proxy.onlyNumbersGoHere) // 1
proxy.onlyNumbersGoHere = '2'
console.log(proxy.onlyNumbersGoHere) // 1
proxy.onlyNumbersGoHere = 3
console.log(proxy.onlyNumbersGoHere) // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 函数与构造函数参数验证
function max(...nums) {
return Math.max(...nums)
}
const proxyMax = new Proxy(max, {
apply(target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided'
}
}
return Reflect.apply(...arguments)
},
})
console.log(max(3, 6, 1)) // 6
console.log(max(3, '6', 1)) // 6
console.log(proxyMax(3, 6, 1)) // 6
console.log(proxyMax(3, '6', 1)) // throw 'Non-number argument provided'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注:《JS高级程序设计》示例有误,书上示例为
apply(target, thisArg, ...argumentsList) {
}
2
这里的扩展运算符是多余的,要去掉。
# 数据绑定与可观察对象
比如,可以将被代理的类绑定到一个全局实例集合中。
const userList = []
class User {
constructor(name) {
this.name = name
}
}
const UserProxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments)
userList.push(newUser)
return newUser
},
})
new UserProxy('jack')
new UserProxy('rose')
console.log(userList) // [ User { name: 'jack' }, User { name: 'rose' } ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19