代理与反射
ES6新增的代理与反射向开发者提供了拦截并向基本操作嵌入额外行为的能力。由于ES6之前不存在,且没有能够将代理转化为之前ES代码的转译程序存在,因此只在百分百支持的平台上有用,可以检测代理是否存在,不存在则提供后备艾玛
9.1. 代理基础
代理是目标对象的抽象。可以用作目标对象的替身,但又完全独立于目标对象。目标对象即可直接被操作,也可以通过代理操作,但直接操作绕过了代理施予的行为
9.1.1. 创建空代理
空代理即作为一个抽象的目标对象之外不做其它任何事,在代理对象上执行的所有操作都会无障碍地传播到目标对象
代理使用Proxy
构造函数创建。接收两个必须的参数,任一缺失都报错TypeError:目标对象和处理程序对象
创建空代理,可以传一个简单的对象字面量作为处理程序对象,从而使得所有操作畅通无阻地抵达目标对象
// 创建目标对象
const target = {
id: 'target'
}
// 创建处理程序对象
const handler = {}
// 创建代理
const proxy = new Proxy(target, handler)
// id属性访问同一个值
console.log(target.id) // target
console.log(proxy.id) // target
// 给目标属性赋值反应在两个对象上,因为访问的是同一个值
target.id = 'foo'
console.log(target.id) // foo
console.log(proxy.id) // foo
// 给代理属性赋值会反应在两个对象上,因为该赋值会转移到目标对象
proxy.id = 'bar'
console.log(target.id) // bar
console.log(proxy.id) // bar
// hasOwnProperty()方法在两者上都会应用到目标对象
console.log(target.hasOwnProperty("id")) // true
console.log(proxy.hasOwnProperty("id")) // true
// Proxy.prototype是undefined,因此不能对其使用instanceof
console.log(target instanceof Proxy) // Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy) // Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy) // false
console.log(target == proxy) // false
9.1.2. 定义捕获器
使用代理的主要目的是定义捕获器,捕获器trap是操作系统中借用的概念,即程序流中一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
捕获器就是在处理程序对象中定义的基本操作的拦截器,每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象前先调用捕获器函数,从而拦截并修改相应的行为
只有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行仍然会产生原定义的正常行为
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
console.log(target['foo'])// bar
console.log(proxy['foo'])// handler override
console.log(Object.create(target)['foo'])// bar
console.log(Object.create(proxy)['foo'])// handler override
9.1.3. 捕获器参数和反射API
所有捕获器都可以访问相应的参数,并基于这些参数可以重建被捕获方法的原始行为。可以通过调用全局Reflect对象上(封装了原始行为)的同名函数来重建,处理程序对象中所有可捕获的方法都有对应的反射Reflect API方法。这些方法与捕获器拦截的方法具有相同名称以及函数签名,也具有与被拦截方法相同的行为。
const target = {
foo: 'bar'
}
const handler = {
get() {
return Reflect.get(...arguments)
}
}
// 以下与上方等价
// const handler = {
// get: Reflect.get
// }
const proxy = new Proxy(target, handler)
console.log(proxy.foo) // bar
console.log(target.foo) // bar
// 创建一个可以捕获所有方法,然后将每个方法转发给对应反射 API 的空代理,那么甚至不需要定义处理程序对象
const target = {
foo: 'bar'
}
const proxy = new Proxy(target, Reflect)
console.log(proxy.foo) // bar
console.log(target.foo) // bar
反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法
const target = {
foo: 'bar',
baz: 'qux'
}
const handler = {
get(trapTarget, property, receiver){
let decoration = ''
if(property === 'foo'){
decoration += '!!!'
}
return Reflect.get(...arguments) + decoration
}
}
const proxy = new Proxy(target, handler)
console.log(proxy.foo)// bar!!!
console.log(target.foo)// bar
console.log(proxy.baz)// qux
console.log(target.baz)// qux
9.1.4. 捕获器不变式
捕获器集合可以改变所有基本方法的行为,但是有限制。每个捕获的方法都知道目标对象上下文、捕获函数签名,被捕获处理程序的行为必须遵循捕获器不变式(trap invariant),通常会防止捕获器定义出现过于反常的行为
// 若目标对象有一个不可配置不可写的数据睡醒,在捕获器返回一个与该属性不同的值时,会抛出TypeError
const target = {}
Object.defineProperty(target, 'foo', {
configurable: false,
writable: false,
value: 'bar'
})
const handler = {
get() {
return 'qux'
}
}
const proxy = new Proxy(target, handler)
console.log(proxy.foo)// Uncaught TypeError: 'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'bar' but got 'qux')
9.1.5. 可撤销代理
需要中断代理对象与目标对象之间的联系,使用new Proxy()创建的普通代理联系会在代理对象的生命周期内一直持续存在
Proxy
暴露了revocable()
方法,该方法支持撤销代理对象与目标对象的关联。撤销操作不可逆,且撤销函数(revoke())是幂等的,调用多次结果都相同。撤销后再调用代理报错TypeError
撤销函数和代理对象是在实例化时同时生成的
const target = {
foo: 'bar'
}
const handler = {
get() {
return 'intercepted'
}
}
// 其中proxy为代理对象,revoke为撤销函数
const { proxy, revoke } = Proxy.revocable(target, handler)
console.log(proxy.foo)// intercepted
console.log(target.foo)// bar
revoke()
console.log(proxy.foo)// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
9.1.6. 实用反射API
1. 反射API和对象API
- 反射API并不限于捕获处理程序
- 大多数反射API方法在Object类型上有对应方法
Object上方法适用于通用程序,反射方法适用于细粒度的对象控制与操作
2. 状态标记
某些反射方法返回一个称为“状态标记”的布尔值,标识意图执行的操作是否成功
// 原代码
const o = {}
try {
Object.defineProperty(o, 'foo', 'bar')
console.log('success')
} catch(e){
console.log('failure')
}
// 修改后的代码
const o = {}
if(Reflect.defineProperty(o, 'foo', {value:'baz'})){
console.log('success')
}else {
console.log('failure')
}
提供状态标记的反射方法:
- Reflect.defineProperty()
- Reflect.preventExtensions()
- Reflect.setPrototypeOf()
- Reflect.set()
- Reflect.deleteProperty()
3. 用一等函数替代操作符
以下反射方法提供只有通过操作符才能完成的操作
- Reflect.get() 替代对象属性访问操作符
- Reflect.set() 替代=赋值操作符
- Reflect.has() 替代in操作符或with()
- Reflect.deleteProperty() 替代delete操作符
- Reflect.construct() 替代new操作符
4. 安全地应用函数
在通过 apply
方法调用函数时,被调用的函数可能也定义了自己的 apply
属性(虽然可能性极小)。为绕过这个问题,可以使用定义在 Function
原型上的 apply
方法
Function.prototype.apply.call(myFunc, thisVal, argumentList)
//这种可怕的代码完全可以使用 Reflect.apply 来避免:
Reflect.apply(myFunc, thisVal, argumentsList)
9.1.7. 代理另一个代理
可以创建一个代理,通过它去代理另一个代理。这样就可以在一个目标对象之上构建多层拦截网
const target = {
foo: 'bar'
}
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy')
return Reflect.get(...arguments)
}
})
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy')
return Reflect.get(...arguments)
}
})
console.log(secondProxy.foo)
// second proxy
// first proxy
// bar
9.1.8. 代理的问题与不足
1. 代理中的this
方法中的this一般指向调用这个方法的对象。若目标对象依赖于对象标识,则容易遇到意料之外的问题
如使用目标对象作为WeakMap的键,代理对象试图使用自身作为键取得这个实例,就会报错。可以通过重新配置代理,将对象实例改为代理对象类本身
const wm = new WeakMap();
class User {
constructor(userId) {
wm.set(this, userId);
}
set id(userId) {
wm.set(this, userId);
}
get id() {
return wm.get(this);
}
}
const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined
// 更改
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
2. 代理与内部槽位
有些ES内置类型可能会依赖于代理无法控制的机制。如Date类型方法执行依赖于this上的内部槽位[[NumberDate]],而代理对象上不存在该槽位,且其值不能通过普通的get()以及set()访问到
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object
9.2. 代理捕获器与反射方法
对于在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。不会存在重复捕获的情况
9.2.1. get()
获取属性值的操作中调用get()
捕获器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7L3J8iJ-1642258075523)(assert/9%E4%BB%A3%E7%90%86%E4%B8%8E%E5%8F%8D%E5%B0%84.assert/1641792556028.png)]
9.2.2. set()
set()
捕获器会在设置属性值的操作中被调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gWl4JVf0-1642258075524)(assert/9%E4%BB%A3%E7%90%86%E4%B8%8E%E5%8F%8D%E5%B0%84.assert/1641792682698.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7goWPo7-1642258075525)(assert/9%E4%BB%A3%E7%90%86%E4%B8%8E%E5%8F%8D%E5%B0%84.assert/1641792692718.png)]
9.2.3. has()
has()
捕获器会在 in
操作符中被调用,has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fuFLXTkk-1642258075525)(assert/9%E4%BB%A3%E7%90%86%E4%B8%8E%E5%8F%8D%E5%B0%84.assert/1641792787896.png)]
9.2.4. defineProperty()
defineProperty()捕获器会在 Object(Reflect).defineProperty()中被调用,defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值
参数:target、property、descriptor
捕获器不变式
- 目标对象不可扩展,无法定义属性
- 目标对象有一个可配置属性,不能添加同名的不可配置属性
- 目标对象有一个不可配置属性,不能添加同名的可配置属性
9.2.5. getOwnPropertyDescriptor()
getOwnPropertyDescriptor()捕获器会在 Object.getOwnPropertyDescriptor()中被调用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor()
getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined
参数:target、property
捕获器不变式
- 自有的target.property存在且不可配置,返回一个标识该属性存在的对象
- 自有的target.property存在且可配置,返回标识该属性可配置的对象
- 自有的target.property存在且target不可扩展,返回一个标识该属性存在的对象
- 自有的target.property不存在且target不可扩展,返回undefined表示该属性不存在
- 自有的target.property不存在,不能返回表示该属性可配置的对象
9.2.6. deleteProperty()
deleteProperty()捕获器会在 delete
操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty()
deleteProperty()必须返回布尔值,表示删除属性是否成功,非布尔值会被转化为布尔值
参数:target、property
捕获器不变式
自有的target.property存在且不可配置,不能删除这个属性
9.2.7. ownKeys()
ownKeys()捕获器会在Object.keys()
及类似方法中被调用,对应的反射方法为Reflect.ownKeys()
ownKeys()
必须返回包含字符串或符号的可枚举对象
参数:target
拦截的操作
- Object.getOwnPropertyNames(proxy)
- Object.getOwnPropertySymbols(proxy)
- Object.keys(proxy)
- Reflect.ownKeys(proxy)
捕获器不变式
- 返回的可枚举对象必须包含target所有不可配置的自有属性
- 若target不可扩展,返回可枚举对象必须准确包含自有属性键
9.2.8. getPrototypeOf()
getPrototypeOf()捕获器会在Object.getPrototypeOf()中调用
返回对象或null
参数:target
拦截的操作
- Object.getPrototypeOf(proxy)
- Reflect.getPrototypeOf(proxy)
- proxy._proto_
- Object.prototype.isPrototypeOf(proxy)
- proxy instanceof Object
捕获器不变式
如果 target 不可扩展,则 Object.getPrototypeOf(proxy)唯一有效的返回值就是 Object.getPrototypeOf(target)的返回值。
9.2.9. setPrototypeOf()
setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。对应的反射 API 方法为Reflect.setPrototypeOf()
返回布尔值,表示原型赋值是否成功
参数:target、prototype
捕获器不变式
若target不可扩展,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target)的返回值
9.2.10. isExtensible()
isExtensible()捕获器会在 Object.isExtensible()中被调用
返回布尔值,表示target是否可扩展
参数:target
捕获器不变式
- 若target可扩展,返回true
- 若不可扩展,返回false
9.2.11. preventExtensions()
preventExtensions()捕获器会在 Object.preventExtensions()中被调用
返回布尔值,表示target是否已经不可扩展
参数:target
捕获器不变式
若Object.isExtensible(proxy)是false,则处理程序必须返回true
9.2.12 apply()
apply()捕获器会在调用函数时被调用
返回值无限制
参数:target、thisArg(调用函数时this参数)、argumentsList(调用函数时的参数列表)
target必须是一个函数对象
拦截的操作
- proxy(…argumentsList)
- Function.prototype.apply(thisArg, argumentsList)
- Function.prototype.call(thisArg, …argumentsList)
- Reflect.apply(target, thisArgument, argumentsList)
9.2.13. construct()
construct()捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()
construct()必须返回一个对象
参数:target、argumentsList(传给目标构造函数的参数列表)、newTarget(最初被调用的构造函数)
target必须可以用作构造函数
拦截的操作
- new proxy(…argumentsList)
- Reflect.construct(target, argumentsList, newTarget)
9.3. 代理模式
9.3.1. 跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过
const user = {
name: 'Jake'
}
const proxy = new Proxy(user, {
get(target, property, receiver){
console.log(`Getting ${property}`)
return Reflect.get(...arguments)
},
set(target, property, receiver){
console.log(`Setting ${property}`)
return Reflect.set(...arguments)
}
})
proxy.name// Getting name
proxy.age = 27// Setting age
9.3.2. 隐藏属性
代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举
const hiddenProperties = ['foo', 'bar']
const targetObject = {
foo: 1,
bar: 2,
baz: 3
}
const proxy = new Proxy(targetObject, {// 隐藏在targetObject中包含的hiddenProperties内的属性
get(target, property){
if(hiddenProperties.includes(property)){
return undefined
}
else{
return Reflect.get(...arguments)
}
},
has(target, property){
if(hiddenProperties.includes(property)){
return false
}
else{
return Reflect.has(...arguments)
}
}
})
// get()
console.log(proxy.foo)// undefined
console.log(proxy.bar)// undefined
console.log(proxy.baz)// 3
// has()
console.log('foo' in proxy)// false
console.log('bar' in proxy)// false
console.log('baz' in proxy)// true
9.3.3. 属性验证
因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值
const target = {
onlyNumbersGoHere: 0
}
const proxy = new Proxy(target, {
set(target, property, value, receiver){
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
9.3.4. 函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)]
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList){
console.log(target)// ƒ median(...nums) {
console.log(thisArg)// undefined
console.log(argumentsList)// (3) [4, 7, 1]
for(const arg of argumentsList){
if(typeof arg !== 'number'){
throw 'Non-number argument provided'
}
}
return Reflect.apply(...arguments)
}
})
console.log(proxy(4, 7, 1))// 4
console.log(proxy(4, '7', 1))// Uncaught Non-number argument provided
// 实例化时必须给构造函数传参
class User {
constructor(id){
this.id_ = id
}
}
const proxy = new Proxy(User, {
construct(target, argumentsList, newTarget){
if(argumentsList[0] === undefined){
throw 'User cannot be instantiated without id'
}
else{
return Reflect.construct(...arguments)
}
}
})
new proxy(1)
new proxy()// Uncaught User cannot be instantiated without id
9.3.5. 数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作
// 将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中
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('John')
new proxy('Jacob')
new proxy('Jingleheimerschmidt')
console.log(userList)
// (3) [User, User, User]
// 0: User {name_: 'John'}
// 1: User {name_: 'Jacob'}
// 2: User {name_: 'Jingleheimerschmidt'}
// [[Prototype]]: Object
// length: 3
// [[Prototype]]: Array(0)
// 把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息
const userList = []
function emit(newValue) {
console.log(newValue)
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver){
const result = Reflect.set(...arguments)// 设置成功则输出消息
if(result){
emit(Reflect.get(target, property, receiver))
}
return result
}
})
proxy.push('John')// John
proxy.push('Jacob')// Jacob
midt’)
console.log(userList)
// (3) [User, User, User]
// 0: User {name_: ‘John’}
// 1: User {name_: ‘Jacob’}
// 2: User {name_: ‘Jingleheimerschmidt’}
// [[Prototype]]: Object
// length: 3
// [[Prototype]]: Array(0)
```js
// 把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息
const userList = []
function emit(newValue) {
console.log(newValue)
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver){
const result = Reflect.set(...arguments)// 设置成功则输出消息
if(result){
emit(Reflect.get(target, property, receiver))
}
return result
}
})
proxy.push('John')// John
proxy.push('Jacob')// Jacob