Proxy 基本概念和使用
基础概念
注意的是,只有访问代理对象(而不是访问被代理的原始对象),拦截操作才会生效。
Proxy
是一个代理对象,这个代理对象可以拦截对某个对象的操作,从而改变其行为。
Proxy
可以理解成,在目标对象之前架设一层拦截 ,外界对该对象的访问,都必须先通过这层拦截。
因此提供了一种机制,可以对外界的访问进行过滤和改写。
new Proxy(targetObj, handler)
,其构造器一共有两个参数,第一个参数是被代理的原始对象,第二个参数的类型也是一个对象,不过其中定义的属性是对原始对象拦截的行为。
例如:new Proxy({}, { get(){}, set(){} ... })
Proxy
目前一共提供 13
种支持的可拦截的行为,下面是 Proxy
支持的拦截操作一览,一共 13 种。
下文的proxy
(首字母小写)指的都是通过new Proxy()
生成的代理对象的实例。
-
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo
和proxy['foo']
;
-
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值;
-
has(target, propKey):拦截propKey in proxy
的操作,返回一个布尔值;
-
deleteProperty(target, propKey):拦截delete proxy[propKey]
的操作,返回一个布尔值;
-
ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
;
Object.getOwnPropertySymbols(proxy)
;
Object.keys(proxy)
;
for...in
循环;
返回一个代表对象属性的数组,并且该数组的成员类型只能是字符串或者Symbol
,不然会有抛出异常;
-
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象;
-
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值;
-
preventExtensions(target):拦截Object.preventExtensions(proxy)
,返回一个布尔值;
-
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy)
,返回一个对象;
-
isExtensible(target):拦截Object.isExtensible(proxy)
,返回一个布尔值;
-
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截;
-
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
;
-
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
;
参考:https://es6.ruanyifeng.com/#docs/proxy 。
使用入门
后面我们就常用的三个拦截行为,get, set, apply
进行详细的描述,其他的拦截行为可以参考:https://es6.ruanyifeng.com/#docs/proxy 。
handler - get
get
方法用于拦截某个属性的读取操作,可以接受三个参数。
依次为目标对象、属性名和 Proxy
实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
使用案例
-
实现数组读取负数的索引;
-
实现属性的链式操作
核心思路有两点:
- 为了满足链式调用,且每次都能触发
proxy
实例的拦截行为,那么需要在需要链式调用的地方每次都要返回当前 proxy
实例;
- 为了能够使得操作能够被保存起来,待到特定时机再返回结果,那么就需要设计一个缓存列表,将操作行为按照顺序缓存起来,待到需要时,再按顺序触发;
第三个参数 receiver
get
的第三个参数 receiver
指的是直接的读操作所在的那个对象,也就是直接访问该属性的对象 (跟这个属性实际属于谁没关系)。
handler - get 的限制
当一个属性的描述符中的 writable: false
或者 configurable: false
,那么 proxy
中的 get
可能会有问题。
具体参考:Object.defineProperty。
handler - set
set
方法用来拦截某个属性的赋值操作。
可以接受四个参数,依次为目标对象、属性名、属性值和 赋值的原始对象(也就是到底是哪个对象直接赋的值, 一般来说是 Proxy
实例),其中最后一个参数可选。
使用案例
-
假定 Person
对象有一个 age
属性,该属性应该是一个不大于 200
的整数,那么可以使用 Proxy
保证 age
的属性值符合要求;
-
通过 get, set
的配置,禁止对象内部私有属性被外部访问读写,私有属性名的第一个字符使用下划线开头,例如:_xxx
;
第四个参数 receiver
set
的第四个参数 receiver
指的是直接的写操作所在的那个对象,也就是直接访问该属性的对象 (跟这个属性实际属于谁没关系)。
handler-set 的限制
如果目标对象自身的某个属性不可写,那么 set
方法中对原始对象值改变的操作不会生效,这里也不会报错,这是由于 writable: false
的特性。
具体参考:Object.defineProperty。
在严格模式下(这里的严格模式最好声明在脚本顶端),set
中必须有返回值,且返回值必须是 true
,不然会抛异常。
handler - apply
要注意的是,这里的 new Proxy(xxx, { apply(){} })
,中的第一个参数 xxx
应该是一个函数,也就是被代理者的类型应该是一个函数。
handler-apply
方法拦截函数的调用、call
和apply
操作。
handler-apply
方法可以接受三个参数,分别是目标函数、目标对象的上下文对象this
和目标对象的参数数组。
这样就比较容易实现装饰者模式, 对某个方法进行增强。
使用案例
-
给一个求和函数 sum
,在不改变内部sum
内部代码的情况下,让其结果翻倍。
-
封装一个 aop
的切面函数,改函数需要满足的功能:能够指定前置,后置,环绕切面函数,并且还能改变原函数的参数。
Proxy.revocable()
生成 Proxy
实例的方式除了 new Proxy(target, handler)
之外。
还可以通过 const { proxy, revoke } = Proxy.revocable(target, handler)
来生成,在对返回值的
解构中, proxy
是对应的代理对象的实例, 而 revoke
是一个函数,可以取消和它对应的 Proxy
实例。
参考:https://es6.ruanyifeng.com/#docs/proxy#Proxy-revocable 。
Proxy 中的 this
注意是:
- 只有函数中才有
this
这个概念;
- 调用某对象的函数,也是先获取这个函数(这里会触发
get
),然后再去调用该函数;
注意两个点:
-
proxy
中 handler
中的 this
指向 handler
对象;
-
被代理对象方法中的 this
指向该属性的直接调用者;
正是因为这个原因,在被代理对象中的一些方法如果对 this
指向有限制的话,那么就会出现错误:
参考:https://es6.ruanyifeng.com/#docs/proxy#this-%E9%97%AE%E9%A2%98 。