在 ES6 之前我们很难模仿一个内置的对象的行为,例如模仿数组,当添加一个新的元素时,length 的长度 +1,当改变 length 的大小时,也会对数组中的元素产生影响,我们很难通过现有的语法去做到这件事情,在 ES6 中给我们提供了 Proxy,使得开发者能够创建内建的对象。
Proxy 可以为对象做一层代理,拦截对象的一些操作,例如为对象的属性赋值,访问对象的属性值,使用 in 操作判断对象是否存在某个属性等等,这些操作都可以被拦截,从而可以对要访问的属性进行验证从而决定采用什么行为。
let target = {}; |
上述对 proxy 的操作会被无条件的转发到 target,proxy 中并不会存储 name 这个属性,所有的操作都是通过转发到 target 完成的。所以当我们为 proxy.name 赋予一个新值,这个操作会被转发到target,也就是为 target.name 赋予一个新值,当我们访问 proxy.name 属性的时候,这个操作也会被转发到 target,返回的是 target.name 的值。
Proxy 可以拦截下面的一些行为
下面就具体介绍用法。
set
当我们为对象的属性赋值时,可以通过 set 方法来拦截这个行为,set 方法接收四个参数:
- trapTarget:target对象
- key:要写入的属性
- value:被写入的属性值
- receiver:操作发生的对象,例如 proxy.name 设置属性值时,receiver 就是 proxy
let target = {}; |
上面我们拦截了为属性赋值的请求,如果当我们为 name 属性赋值时,那么什么都不做,否则就直接将操作转发给 target,上面 Reflect.set(traptarget, key, value, receiver)
就是将操作转发给target,Reflect.set 接收的参数与 set 方法一样
proxy.name = "Alice"; |
上面我们通过 proxy 为 name 属性赋值,发现并没有成功,打印出的结果是 undefined,因为我们拦截了赋值请求,无法为 name 属性赋值;但是当我们为其他属性赋值时,它会被直接转发到 target,所以赋值时成功的。
get
当我们访问对象的属性值时,可以通过 get 方法拦截这个请求,get 方法接收三个参数
- trapTarget
- key
- receiver
参数的含义同上,不多做解释。
let proxy = new Proxy(target, { |
上面我们拦截了访问对象属性值的请求,首先我们会查找对象中是否存在这个属性,如果不存在则会抛出错误(而不是返回 undefined),如果存在则直接通过 Reflect.get 转发给 target
proxy.gender = "male"; |
has
当我们使用 in 操作符判断对象中是否存在某个属性时,我们可以使用 has 方法拦截这个请求,has 方法接收两个参数
- trapTarget
- key
let target = { |
当查找的 key 的值是 age 时,我们直接返回 false,否则直接转发到 target 上
console.log("name" in proxy); // true |
deleteProperty
delete 操作用以删除对象的属性,我们可以通过 deleteProperty 来拦截这一行为,deleteProperty 接收两个参数
- trapTarget
- key
let target = { |
当我们想删除 age 属性时,直接返回 false 而不做删除操作,表示删除失败,当想删除其他属性时,则将操作转发给 target
console.log(proxy.age); // 18 |
对象原型
我们可以通过 Object.setPrototypeOf 和 Object.getPrototypeOf 为对象设置原型以及访问对象的原型,我们可以通过 setPrototypeOf 与 getPrototypeOf 来拦截这个行为。setPrototypeOf 接收两个参数:
- trapTarget
- proto:原型
setPrototypeOf 返回 false 表示设置原型失败,返回任一非 false 值 Object.setPrototype 会认为原型设置成功
let target = {}; |
上面当我们设置原型时,如果原型对象没有 name 属性,我们返回 false,表示原型设置失败
let proto = { |
getPrototype 接收一个参数:
- tarpTarget
getPrototype 必须返回一个对象或者 null,否则会报错
let target = { |
如果对象具有 name 属性,我们就直接返回 null,否则走默认行为
console.log(Object.getPrototypeOf(proxy)); // null |
对象扩展
Object.isExtensible 是判断对象是否可扩展的,而 Object.preventExtensions 是用来设置对象不可扩展的,我们可以通过 isExtensible 和 preventExtensions 来拦截默认行为,它们都接收一个参数
- trapTarget
且返回一个布尔值,isExtensible 返回的布尔值表示十分可扩展,而 preventExtensions 返回的布尔值表示是否设置不可扩展,返回 false 表示不设置不可扩展。
isExtensiable 返回的值一定要与 Object.isExtensible 和 Reflect.isExtensible 返回的值相同,当 preventExtensions 返回 false 时,Object.preventExtensions 会认为设置失败,从而抛出一个错误。
let target = {} |
对象描述符
当我们调用 Object.defineProperty 时,我们可以通过 defineProperty 来拦截这一行为,defineProperty 接收三个参数
- trapTarget
- key
- descriptor
let target = { |
上述拦截了 defineProperty,当配置的键为 Symbol 类型时,返回 false,表示不可配置,而对于字符串类型的键则调用 Reflect 转发给 target 对象
Object.defineProperty(proxy, "age", { |
Object.getOwnPropertyDescriptor 方法使用获得属性的描述符的,同样可以使用 getOwnPropertyDescriptor 进行拦截,它接收两个参数
- trapTarget
- key
返回值可以是 undefined 或者一个对象,如果返回的是一个对象,那么对象中的属性只能包括
- enumerable
- configurable
- value
- writable
- get
- set
如果返回的对象除了其他的属性,那么Object.getOwnPropertyDescriptor 会抛出一个错误
let proxy = new Proxy(target, { |
上面的程序表示,无法获得到键为 Symbol 类型的描述符。
ownKeys
当我们调用 Object.keys
,Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
,Object.assign
时,首先会在内部调用 [[OwnPropertyKeys]]
来得到一个数组用于上述方法筛选,Object.keys
,Object.getOwnPropertyNames
返回的结果将 Symbol 类型的值排除在外,Object.getOwnPropertySymbols
将字符串类型的值排除在外,Object.assign
两种类型都支持。
我们通过 ownKeys
这个方法来拦截 [[OwnPropertyKeys]]
的行为,它只接受一个参数
- trapTarget
返回值必须为数组或者类数组,否则抛出错误。
let target = { |
上面的程序过滤掉了以下划线开头的属性,因为这样的属性表示私有属性,不想被访问到。
apply 和 construct
Proxy 还可以用来代理函数,可以拦截函数的调用。根据调用的方式不同,有两个拦截的方法
- apply:正常调用函数
- construct:通过 new 调用函数
apply 方法接收三个参数
- trapTarget:执行的函数
- thisArgument:函数内部的 this
- argumentsList:传入函数的参数,以数组的形式显示
construct 方法接收两个参数
- trapTarget
- argumentsList
let target = function(...values) { |
上面的程序利用 apply 截获了函数调用来做参数校验,如果传入的参数不全是数字的话,那么就会抛出错误。
有的时候函数只能通过 new 的方式调用,这个时候我们会判断 new.target 变量来纠正行为,但是我们还可以通过截获普通函数调用,然后返回一个 new 调用的结果
function Person(name) { |
这个时候无论是普通调用还是 new 调用,返回的都是 Person 的实例对象。
撤销代理
上面创建的代理对象都是不可撤销,就是说它一直代理着目标对象,如果我们希望代理在某个时刻失效,那我们就需要撤销代理,我们可以通过 Promise.revecable 方法创建能够撤销的代理对象,它接收的参数同 Proxy 构造函数相同,返回一个对象,对象中具有两个属性:
- proxy:可被撤销的代理对象
- revoke:调用该方法可撤销代理,调用该方法后代理对象不可用
let target = { |