Proxy-Reflect使用

Proxy

查看

需要:如果存在一个对象,我们需要去监听这个对象中的属性被设置或获取的过程,应该如何操作?

在ES6之前,我们可以通过Object.defineProperty()的存取属性描述符来对属性的操作进行监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = { name: 'obj', age: 18 }

Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log(`${key}的值为${value}`)
},
set(newValue) {
console.log(`${key}的值发生了改变:${value} => ${newValue}`)
value = newValue
}
})
})

// 对属性进行赋值操作时,执行其对应set访问器
obj.name = 'obj1' // name的值发生了改变:obj => obj1
// 对属性进行调用,执行其对应get访问器
obj.name // name的值为obj1

但这样是存在缺点的:

  • Object.defineProperty设计的初衷并不是去为了监听对象的所有属性的,我们在定义某些属性的时候,初衷只是为了定义普通的属性,只是因为其存在访问器这种特点,就强行用属性描述符去操作了。

  • Object.defineProperty只能监听属性的存取,但如果我们想监听更加丰富的操作,比如新增、删除属性等,那么Object.defineProperty是无能为力的。

ES6中,新增了Proxy类,用于帮助我们创建一个代理对象,从而对对象操作进行监听。

Proxy语法

const p = new Proxy(target, handler)
target:要创建代理的对象
handler: 是定义了代理的自定义行为的对象

如果我们想要侦听某些具体的操作,那么可以在handler中添加对应的捕获器。

对于上面的案例,通过Proxy实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = { name: 'obj', age: 18 }

const proxy = new Proxy(obj, {
// target为目标对象,key为修改的属性名
get(target, key) {
console.log(`${key}的值为${target[key]}`)
return target[key]
},
// target为目标对象,key为修改的属性名,value为待修改的值
set(target, key, value) {
console.log(`${key}的值发生了改变:${target[key]} => ${value}`)
target[key] = value
}
})
// 通过代理对象对属性进行赋值操作时,执行其set捕获器,并对源对象的进行对应修改,name的值发生了改变:obj => obj1
proxy.name = 'obj1'
console.log(obj.name) // obj1
// 通过代理对象获取其属性时,执行器get捕获器,name的值为obj1
proxy.name

Proxy所有捕获器

这里说一下construct捕获器和apply捕获器,这两个捕获器是应用于函数对象的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log(this)
}
const proxy = new Proxy(foo, {
construct(target, argumentsList, newTarget) {
console.log(argumentsList, newTarget)
return new target(...argumentsList)
},

apply(target, thisArg, argumentsList) {
console.log(target, thisArg, argumentsList)
return target.apply(thisArg, argumentsList)
}
})
const p = new proxy(1, 2)
proxy.apply('111', [1, 2, 3])

handler.construct(target, argumentsList, newTarget)
参数:
target:被代理的函数
argumentsList:创建实例时传入的参数
newTarget:最初被调用的构造函数,上述为proxy对象

apply(target, thisArg, argumentsList)
apply时调用函数的捕获器,包括普通调用,apply/call调用参数:
target:被代理的函数
thisArg:调用函数时传入的this
argumentsList:调用函数时传入的参数

Reflect

查看

Reflet是ES6新增的一个API,它提供了许多操作对象的方法,Object的方法基本都可以通过Reflect实现,用法和Object相同,但是Reflect的方法更简洁,并且更符合语义。

比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf(),Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()。

那么我们可能会有一些疑惑,既然Reflect可以做的,Object都可以做,为什么还要有Reflect呢?

  • 由于在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计更加规范,所以将相应API都放在了Object上面。

  • 但是Object的API设计存在一些问题,一些是静态方法(如Object.keys()、Object.defineProperty()),一些又是实例方法obj.hasOwnProperty,这样调用方法不一致的问题,增大了学习者的心理负担。

  • Object的API的错误处理不统一,某些方法如Object.defineProperty()是通过抛出异常处理错误,而某些方法是给出返回值,如Object.freeze()当传入一个原始值时,如传入一个数字,并不会抛出错误,而是将其转换为对应的包装类,然后将其返回。

  • 对this绑定存在隐患,Object的实例方法依赖this绑定,若方法被错误调用,如提取后单独使用,会导致意外行为。

  • 对于Object的实例方法易被重写。

总的来说,对于Object的API并没有进行统一的规范,操作混乱,容易出错。

Reflect的出现可以说是Object的操作增强。

  • 统一的操作函数,Reflect将所有对象的操作静态化,解决了Object的一会静态,一会实例方法的API设计规范问题。

  • 合理返回值设计,对于不合法的调用,Reflect会返回true/false,解决了Object的返回值不统一的问题。

  • 填补了空白功能,如Object要获取对象的键是,需要通过obj.getOwnPropertyNames()obj.getOwnPropertySymbols(),但Reflect提供了Reflect.ownKeys是上述两种方法的组合。

Reflect的常用方法:

Proxy和Reflect结合使用

查看

上文我们说了Reflect和Object的区别,Reflect的出现标准化了对象的底层操作(提供一致的方法接口)和弥补了Object方法的不足(如错误处理和函数式风格)。

但Reflect还有一个应用场景,就是与Proxy结合使用(支持Proxy的陷阱(捕获器)实现)。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
_name: '111',
get name(){
return this._name
}
}

const proxy = new Proxy(obj, {
get(target, key) {
console.log(key)
return target[key]
},
})

proxy.name

以上代码get捕获器只触发了一次,即只触发了对name属性的获取,但按理来说,对此对象的所有属性的获取都应该触发get捕获器,但只触发获取name的访问器,这是由于this指向的问题,我们在get捕获器中通过操作源对象的方式获取属性值,导致this并没有指向代理对象,这破坏了代理的封装性。

在看一个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
_name: '111',

set name(value) {
this._name = value
}
}

const proxy = new Proxy(obj, {
set(target, key, value) {
console.log(key)
target[key] = value
}
})

proxy.name = '222'

以上代码set捕获器只触发了一次,和上述的get捕获器一样,都是捕获器中直接操作源对象的结果,破坏了代理的封装性。

其实Proxy的get/set捕获器中还有一个receiver参数,receiver指向当前代理对象,而Reflect的get/set方法也可以接受一个receiver参数,通过receiver可以保证proxy的陷阱实现的正确性。

所以可以对以上代码进行修改,将Proxy和Reflect结合使用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = {
_name: '111',
get name() {
return this._name
},
set name(value) {
this._name = value
}
}

const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log(key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(key)
Reflect.set(target, key, value, receiver)
}
})
proxy.name = '222'
proxy.name

补充:

在ES5我们实现继承时有借用构造函数思想,即改变this指向,实现对父类构造函数的调用。
Reflect提供了相同操作的方法:Reflect.construct(target, argumentsList, newTarget)
参数:
target: 借用构造函数的对象
argumentsList: 构造函数的参数
newTarget: 需要创建实例的构造方法/类

关于Proxy和Reflect结合的思考

查看

上文我们已经了解到,Proxy的是对象的代理,通过对其自定义行为对象中添加相应的捕获器,可以监听到对象相应的的操作,Reflect实现了对对象操作的规范化,弥补了对象的不足,同时将Proxy和Reflect结合使用,可以保证Proxy的陷阱的实现正确性,保证了Proxy的代理封装性。

Proxy与被代理对象的数据是同步的,因为Proxy最终读取始终是被代理对象的属性,即使不通过Proxy对源对象进行修改。

Reflect操作源对象的get/set方法是对对象的内部方法[[Get]]/[[Set]]的显示封装,而通过obj.[key]则是对其的隐式封装,但对其的操作容易受this的影响而变为对其他对象的操作,而Reflect可以通过receiver保证this指向的正确性,放如Proxy中,既可以保证this永远指向receiver,从而实现对源对象的[[Get]]/[[Set]]操作都会走Proxy进而触发捕获器。

即当不存在访问器属性时,this其实与[[Get]]/[[Set]]的操作并无关系,从Reflect.get(target, key, receiver)中可以看出,对属性值的获取其值只需target和key就行,通过target的内部方法[[Get]]即可获取到属性值,而不需要this,当存在访问器时,通过receiver只是提供正确的this指向代理对象,同时也可以看出Proxy的操作一直就是对target(被代理对象)的操作,而不是对Proxy本身的操作,所以默认情况下,Proxy保持与被代理对象的数据同步,但如果Proxy在捕获器中进行了自定义操作(未对对象进行相应操作),代理对象与被代理对象就不一定数据同步了。