ES2015 Proxy代理对象与Reflect反射对象
Proxy 代理对象
如果想要监视某个对象中属性的读写,可以使用 ECMAScript2015 之前的 Object.defineProperty()
方法为对象添加属性,这样的话就可以监测到对象属性的读写过程。这种方法应用的非常广泛,在 Vue3.0 之前的版本就是使用这样的方法来实现数据响应,从而完成双向数据绑定。
在 ECMAScript2015 当中设计了 Proxy
类型,它就是专门为对象设置访问代理器的。如果你不能理解什么是代理的话,你可以把它想象成门卫。也就是说,无论你是从里面拿东西还是放东西,都需要经过这样的一个代理。通过 Proxy
就可以轻松地监视到对象属性的读写过程。相对于 Object.defineProperty()
方法,Proxy
的功能更为强大,使用起来也更为方便。
接下来,就来看看 Proxy
的具体用法。首先,通过字面量方式来定义一个 person
对象。如下代码所示:
const person = {
name: '前端课湛',
age: 20
}
然后,通过 new Proxy()
方式为 person
对象创建一个代理对象。如下代码所示:
const personProxy = new Proxy(person, {
get() {},
set() {}
})
在上述代码中,Proxy
构造函数中接收了两个参数,第一个参数就是代理的目标对象,第二个参数也是一个对象,可以称之为代理的处理对象。
这个代理的处理对象中的 get()
方法可以用来监视属性的访问,set()
方法可以用来监视属性的设置。
接下来,先看 get()
方法,这个方法可以接收 target
和 property
两个参数,这个方法的返回值是作为外部访问这个属性所得到的结果。如下代码所示:
const personProxy = new Proxy(person, {
get(target, property) {
console.log(target, property)
return 100
},
set() {}
})
console.log(personProxy.name)
上述代码的运行结果如下:
{ name: '前端课湛', age: 20 } name
100
从打印的结果可以看到,这时的 get()
方法已经监听到了属性的读取。其中 target
参数指的是代理的目标对象,property
参数指的是外部访问对象的属性名,而返回值就是访问属性之后所得到的结果。
get()
方法内部正常的逻辑应该是先来判断代理目标对象当中是否存在这样一个属性,如下代码所示:
const personProxy = new Proxy(person, {
get(target, property) {
return property in target ? target[property] : undefined
},
set() {}
})
console.log(personProxy.name)
console.log(personProxy.xxx)
如果访问的属性存在的话,则返回该属性的值;如果不存在的话,则返回 undefined
或者提供的默认值。
然后,再来看一下 set()
方法,这个方法可以接收 target
、property
和 value
三个参数。如下代码所示:
const personProxy = new Proxy(person, {
get() {},
set(target, property, value) {
console.log(target, property, value)
}
})
personProxy.gender = true
上述代码的运行结果如下:
{ name: '前端课湛', age: 20 } gender true
从打印结果可以看到,target
参数依旧表示代理的目标对象,property
参数表示设置的属性名,而 value
参数就是设置的属性值。
set()
方法内部正常的逻辑应该是为代理的目标对象设置对应的属性。如下代码所示:
const personProxy = new Proxy(person, {
get() {},
set(target, property, value) {
if (property === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError(`${value} is not an integer.`)
}
}
target[property] = value
}
})
personProxy.age = 'hehehe'
这里的 set()
方法来判断设置的属性名是否为 age
,如果是的话必须是整数,否则就报错。通过 personProxy
代理对象设置 age
属性值为 hehehe
。运行的结果如下:
proxy.js:53
throw new TypeError(`${value} is not an integer.`)
^
TypeError: hehehe is not an integer.
at Object.set (proxy.js:53:15)
at Object.<anonymous> (proxy.js:60:17)
at Module._compile (internal/modules/cjs/loader.js:1201:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1221:10)
at Module.load (internal/modules/cjs/loader.js:1050:32)
at Function.Module._load (internal/modules/cjs/loader.js:938:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47
以上就是 Proxy
的一些基本用法,以后 Proxy
会越用越多,比如 Vue3.0 开始就已经使用 Proxy
来实现内部的数据响应。
Proxy 的优势
了解了 Proxy
的基本用法过后,接下来再深入探索一下相比于 Object.defineProperty()
来讲 Proxy
到底有哪些优势。
监听对象的更多操作
首先,最明显的优势在于 Proxy
更为强大一些。这个强大在于 Object.defineProperty()
只能监视属性的读写,而 Proxy
能够监视到 Object.defineProperty()
监视不到的更多对象的操作,比如 Delete
操作、调用对象的方法等等。如下代码所示:
const person = {
name: '前端课湛',
age: 20
}
const personProxy = new Proxy(person, {
deleteProperty(target, property) {
console.log(`delete ${property}`)
delete target[property]
}
})
delete personProxy.age
console.log(person)
上述代码为 person
对象创建了一个代理对象,并且通过代理对象的 deleteProperty()
方法监听 person
对象属性的删除操作。
上述代码的运行结果如下:
delete age
{ name: '前端课湛' }
从运行结果可以看到,Proxy
成功地监听到 Delete
操作。
Proxy 除了可以监视到对象的 Delete 操作以外,还有很多其他操作。如下表所示:
handler 方法 | 触发方式 |
---|---|
get | 读取某个属性 |
set | 写入某个属性 |
has | in 操作符 |
deleteProperty | delete 操作符 |
getProperty | Object.getPropertyOf() |
setProperty | Object.setPropertyOf() |
isExtensible | Object.isExtensible() |
preventExtensions | Object.preventExtensions() |
getOwnProperyDescriptor | Object.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() |
ownKeys | Object.keys()、Object.getOwnPropertyName()、Object.getOwnPropertySymbols() |
apply | 调用一个函数 |
construct | 通过 new 调用一个函数 |
更好地支持数组的监听
其次,Proxy
相比于 Object.defineProperty()
可以更好地支持数组对象的监视。以往想要通过 Object.defineProperty()
监视数组对象的操作,最常见的一种方式就是通过重写数组的操作方法,这也是 Vue 所使用的方式。大体的思路就是通过自定义的方法去覆盖数组原型对象上的原有方法,以此来劫持对应方法的调用过程。Proxy 又是如何监视数组对象的呢?如下代码所示:
const list = []
const listProxy = new Proxy(list, {
set(target, property, value) {
console.log('set: ', property, value)
target[property] = value
return true
}
})
listProxy.push('前端课湛')
通过 Proxy
对象的 set()
方法监听数组的 push
操作,并且将指定成员添加到代理的目标数组中,最终返回 true
表示设置成功。
上述代码的运行结果如下:
set: 0 前端课湛
set: length 1
从打印结果可以看到,这里的 0
就是数组的索引值,而前端课湛
就是索引值对应的成员。也就是说,Proxy
内部可以通过数组的 push
操作来推算出来应该所处的索引值。对于数组的其他操作都是类似的,而这些操作如果通过 Object.defineProperty()
来实现的话就会特别麻烦。
非侵入的方式监听
最后,相比于 Object.defineProperty()
来讲 Proxy
的优势就是以非侵入的方式监管了对象的读写。也就是说,一个已经定义好的对象,不需要对对象本身去做任何的操作就可以监视到它内部成员的读写。如下代码所示:
const person = {
name: '前端课湛',
age: 20
}
const personProxy = new Proxy(person, {
get(target, property) {
console.log('get', property)
return target[property]
},
set(target, property, value) {
console.log('set', property, value)
target[property] = value
}
})
personProxy.name = '大前端'
console.log(personProxy.name)
而 Object.defineProperty() 方式要求需要通过特定的方式单独定义对象当中那些需要被监视的属性,那对于一个已经存在的对象想要监视它的属性需要去做很多额外的操作。如下代码所示:
const person = {
name: '前端课湛',
age: 20
}
Object.defineProperty(person, 'name', {
get () {
console.log('name 被访问')
return person._name
},
set (value) {
console.log('name 被设置')
person._name = value
}
})
Object.defineProperty(person, 'age', {
get () {
console.log('age 被访问')
return person._age
},
set (value) {
console.log('age 被设置')
person._age = value
}
})
person.name = '大前端'
console.log(person.name)
Reflect 反射对象
Reflect
是 ECMAScript2015 中提供的全新的内置对象,如果按照 Java 或者 C# 的说法,Reflect
属于一个静态类。也就是说,它不能通过 new Reflect()
构建一个实例对象,只能调用这个静态类提供的静态方法,比如 get()
等等。关于这一点应该不会太陌生,因为 JavaScript 语言中的 Math
也是这样的。
Reflect
内部封装了一系列对对象的底层操作,具体一共提供了 14 个静态方法,其中有一个被废弃了,所以目前还有 13 个。这 13 个方法的方法名与 Proxy
对象当中的处理对象的方法成员是完全一致的,其实 Reflect
的成员方法就是 Proxy
处理对象的那些方法的默认实现。如下代码所示:
const obj = {
foo: '123',
bar: '456'
}
const proxy = new Proxy(obj, {
get(target, property) {
console.log('watch logic~')
return Reflect.get(target, property)
}
})
console.log(proxy.foo)
当 Proxy
的处理对象当中并没有添加任何的成员时,Proxy
内部默认实现的逻辑就是调用了 Reflect
对象当中所对应的静态方法。这和上述代码当中直接将 Proxy
处理对象的 get()
方法直接调用 Reflect
对象的 get()
静态方法是一致的。
Reflect
对象的用法其实很简单,但是大多数人接触到这个对象的感觉就是为什么要有 Reflect
这样的一个对象?也就是说,Reflect
对象的价值到底体现在什么地方?Reflect
对象最大的意义就在于它提供了一套统一的用于操作对象的 API。
在这儿之前如果操作对象的话,有可能会使用 Object
对象上的一些方法,也有可能会使用比如 in
或者 delete
运算符,这些操作对新手来说实在是太乱了,并没有什么规律。如下代码所示:
const obj = {
name: '前端课湛',
age: 20
}
console.log('name' in obj)
console.log(delete obj['age'])
console.log(Object.keys(obj))
而 Reflect 对象就很好地解决了这样的一个问题,它统一了操作对象的方式。如下代码所示:
const obj = {
name: '前端课湛',
age: 20
}
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))
需要注意的是,目前的情况是之前的用法还是可以使用的,但是 ECMAScript 官方希望经过一段时间的过渡过后,以后的标准当中就会把之前的方法废弃掉。