在了解 Proxy 和 Reflect 前,需要先了解属性描述符的作用
属性描述符
属性描述符(Property Descriptor) 本质上是一个 JavaScript 普通对象,用于描述一个属性的相关信息,共有以下几种属性:
-
value:属性值
-
configurable:该属性的描述符是否可以修改
-
enumerable:该属性是否可以被枚举
-
writable:该属性是否可以被重新赋值
-
存取器属性:属性描述符中如果配置了 get 和 set 中的任何一个,则该属性变成了存取器属性
- get()读值函数:get 方法得到的返回值作为属性值
- set(newVal)存值函数:newVal 参数为赋值的值
存取器属性最大的意义,在于可以控制属性的读取和赋值,在函数里可以进行各种操作
PS: value 和 writable 属性不能与 get 和 set 属性二者不可共存,二者只能选其一
常见的方法如下:
1)查看某个对象的属性描述符,使用以下这两种方法:
Object.getOwnPropertyDescriptor(对象, 属性名) //得到一个对象的某个属性的属性描述符
Object.getOwnPropertyDescriptors(对象) //得到某个对象的所有属性描述符
2)为某个对象添加属性时 或 修改属性时,配置其属性描述符,使用以下这两种方法:
Object.defineProperty(对象, 属性名, 描述符) //设置一个对象的某个属性
Object.defineProperties(对象, 多个属性的描述符) //设置一个对象的多个属性
Reflect
Reflect 是一个内置的 JS 对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能。由于它类似于其他语言的反射,因此取名为 Reflect
ES6 认为对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,需要将它们提取出来,形成一个正常的 API,并高度聚合到某个对象中,于是就造就了 Reflect 对象。因此,Reflect 对象中有很多的 API 都可以使用过去的某种语法或其他 API 实现
主要有以下API,其他更多的API
Proxy
ES6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象(target)定义一个关联的代理对象,而这个代理对象可当作一个抽象的目标对象来使用。 因此在对目标对象的各种操作影响到目标对象之前,我们可以在代理对象中对这些操作加以控制,并且最终也可能会改变操作返回的结果
创建空代理
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。 默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象
代理是使用 Proxy 构造函数创建的,这个构造函数接收两个参数:目标对象和处理程序对象,缺少其中任何一个参数都会抛出 TypeError
要创建空代理,可以传一个简单的对象字面量作为处理程序对象
const target = {
id: 'target'
} //target:目标对象
const handler = {} //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) // TypeError: Function has non-object prototype
console.log(proxy instanceof Proxy) // TypeError: Function has non-object prototype
// 严格相等可以用来区分代理和目标
console.log(target === proxy) // false
定义捕获器
使用代理的主要目的是可以定义捕获器(trap),捕获器就是在处理程序对象中定义的“基本操作的拦截器”
每个处理程序对象可以包含 0 个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为
PS:捕获器是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数
只有在代理对象上执行操作才会触发捕获器,在目标对象上执行仍然会产生正常的行为
const target = {
foo: "bar",
}
const handler = {
// 捕获器在处理程序对象中以方法名为键
get(trapTarget, property, receiver) {
//trapTarget - 目标对象
//property - 要查询的属性
//receiver - 代理对象
return "handler override"
},
}
const proxy = new Proxy(target, handler)
console.log(target.foo) // bar
console.log(proxy.foo) // handler override
捕获器不变式
使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制
根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant),捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为
比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
const target = {}
Object.defineProperty(target, "foo", {
configurable: false,
writable: false,
value: "bar",
})
const handler = {
get() {
return "water"
},
}
const proxy = new Proxy(target, handler)
console.log(proxy.foo) // TypeError
可撤销代理
有时候可能需要中断代理对象与目标对象之间的联系,Proxy 也暴露了 revocable() 方法,这个方法支持撤销代理对象与目标对象的关联,后续可直接调用撤销函数 revoke() 来撤销代理,撤销代理这个操作是不可逆的
撤销代理之后再调用代理会抛出 TypeError,撤销函数和代理对象是在实例化时同时生成的:
const target = {
foo: "bar",
}
const handler = {
get() {
return "intercepted"
},
}
const { proxy, revoke } = Proxy.revocable(target, handler)
console.log(proxy.foo) // intercepted
console.log(target.foo) // bar
revoke()
console.log(proxy.foo) // TypeError
代理的问题与不足
代理是在 ES6 现有基础上构建出来的一套新API,很大程度上,代理作为对象的虚拟层可以正常使用,但在某些情况下,代理也不能与现在的 ES 机制很好地协同
代理中的this
方法中的 this 通常指向调用这个方法的对象:
const target = {
thisValEqualsProxy() {
return this === proxy
}
}
const proxy = new Proxy(target, {})
console.log(target.thisValEqualsProxy()) // false(this 指向目标对象)
console.log(proxy.thisValEqualsProxy()) // true(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)
}
}
由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况下就会出问题。 User 实例的 this 是原对象,代理调用时 this 指向代理,导致 WeakMap 查不到键
const user = new User(123)
console.log(user.id) // 123(正常)
const proxy = new Proxy(user, {})
console.log(proxy.id) // undefined(WeakMap 键为原对象,代理无法读取)
要解决这个问题,就需要重新配置代理,改为代理类而非实例,使代理后的实例成为 WeakMap 的键
const UserClassProxy = new Proxy(User, {})
const proxyUser = new UserClassProxy(456) // 代理实例的 `this` 指向自身
console.log(proxyUser.id) // 456(WeakMap 键为代理实例)
代理与内部槽位
某些内置类型(如 Date、Array)的方法依赖内部槽位,而代理无法访问或修改这些槽位,导致方法调用失败
比如下面这个例子,代理拦截后本应转发给目标对象的方法会抛出 TypeError
- Date 的 getDate() 依赖内部槽位 [[NumberDate]],代理对象无此槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 操作访问到
- 即使
proxy instanceof Date
返回true
,代理仍无法转发内部槽位操作
const target = new Date()
const proxy = new Proxy(target, {})
console.log(proxy instanceof Date) // true
proxy.getDate() // TypeError: 'this' is not a Date object
代理捕获器与代理方法
get()
get()捕获器会在获取属性值的操作中被调用,对应的反射 API 方法为 Reflect.get()
1)返回值
- 返回值无限制
2)拦截的操作
- proxy.property
- proxy[property]
- Object.create(proxy)[property]
- Reflect.get(proxy, property, receiver)
3)捕获器处理程序参数
- target:目标对象
- property:引用的目标对象上的字符串键属性
- receiver:代理对象或继承代理对象的对象
4)捕获器不变式
- 如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配
- 如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined
const myTarget = {}
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log("get()")
return Reflect.get(...arguments)
},
})
proxy.foo // 触发get()捕获器
set()
set()捕获器会在设置属性值的操作中被调用,对应的反射 API 方法为 Reflect.set()
1)返回值
- 返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError
2)拦截的操作
- proxy.property = value
- proxy[property] = value
- Object.create(proxy)[property] = value
- Reflect.set(proxy, property, value, receiver)
3)捕获器处理程序参数
- target:目标对象
- property:引用的目标对象上的字符串键属性
- value:要赋给属性的值
- receiver:接收最初赋值的对象
4)捕获器不变式
- 如果 target.property 不可写且不可配置,则不能修改目标属性的值
- 如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。 在严格模式下,处理程序中返回 false 会抛出 TypeError
const myTarget = {}
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log("set()")
return Reflect.set(...arguments)
},
})
proxy.foo = "bar" // 触发set()捕获器
has()
has()捕获器会在 in 操作符中被调用,对应的反射 API 方法为 Reflect.has()
1)返回值
- has()必须返回布尔值,表示属性是否存在,返回非布尔值会被转型为布尔值
2)拦截的操作
- property in proxy
- property in Object.create(proxy)
- with(proxy) {(property);}
- Reflect.has(proxy, property)
3)捕获器不变式
- 如果 target.property 存在且不可配置,则处理程序必须返回 true
- 如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true
const myTarget = {}
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log("has()")
return Reflect.has(...arguments)
},
})
"foo" in proxy //触发 has()捕获器
deleteProperty()
deleteProperty()捕获器会在 delete 操作符中被调用,对应的反射 API 方法为 Reflect.deleteProperty()
1)返回值
- deleteProperty()必须返回布尔值,表示删除属性是否成功,返回非布尔值会被转型为布尔值
2)拦截的操作
- delete proxy.property
- delete proxy[property]
- Reflect.deleteProperty(proxy, property)
3)捕获器不变式
- 如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性
const myTarget = {}
const proxy = new Proxy(myTarget, {
deleteProperty(target, property) {
console.log("deleteProperty()")
return Reflect.deleteProperty(...arguments)
},
})
delete proxy.foo // 触发deleteProperty()捕获器
apply()
apply()捕获器会在调用函数时中被调用,对应的反射 API 方法为 Reflect.apply()
1)返回值
- 返回值无限制
2)拦截的操作
- proxy(…argumentsList)
- Function.prototype.apply(thisArg, argumentsList)
- Function.prototype.call(thisArg, …argumentsList)
- Reflect.apply(target, thisArgument, argumentsList)
3)捕获器处理程序参数
- target:目标对象
- thisArg:调用函数时的 this 参数
- argumentsList:调用函数时的参数列表
4)捕获器不变式
- target 必须是一个函数对象
const myTarget = () => {}
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
console.log("apply()")
return Reflect.apply(...arguments)
},
})
proxy() // 触发apply()捕获器
construct()
construct()捕获器会在 new 操作符中被调用,对应的反射 API 方法为 Reflect.construct()
1)返回值
- construct()必须返回一个对象
2)拦截的操作
- new proxy(…argumentsList)
- Reflect.construct(target, argumentsList, newTarget)
3)捕获器处理程序参数
- target:目标构造函数
- argumentsList:传给目标构造函数的参数列表
- newTarget:最初被调用的构造函数
4)捕获器不变式
- target 必须可以用作构造函数
const myTarget = function () {}
const proxy = new Proxy(myTarget, {
construct(target, argumentsList, newTarget) {
console.log("construct()")
return Reflect.construct(...arguments)
},
})
new proxy() // 触发construct()捕获器
其他捕获器可参考:MDN-Proxy