You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
classAnimal{
#name ='动物'getName(){isLog||console.log(this)returnthis.#name
}}constanimal=newAnimal()constanimalProxy=newProxy(animal,{get(target,key,recevier){returntarget[key]},set(target,key,value,recevier){target[key]=value}})// TypeError: Cannot read private member #name from an object whose class did not declare itconsole.log(animalProxy.getName())
constfactoryInstanceProxy=instance=>newProxy(instance,{get(target,prop){returntarget[prop]},set(target,prop,val){target[prop]=valreturntrue}})// TypeError: Method Map.prototype.set called on incompatible receiver #<Map>constmap=factoryInstanceProxy(newMap())map.set(0,1)// TypeError: this is not a Date object.constdate=factoryInstanceProxy(newDate())date.getTime()// Method Promise.prototype.then called on incompatible receiver #<Promise>constresolvePromise=factoryInstanceProxy(Promise.resolve())resolvePromise.then()// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>constset=factoryInstanceProxy(newSet())set.add(1)
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy被用于许多库和浏览器框架上,例如vue3就是使用Proxy来实现数据响应式的。本文带你了解Proxy的用法与局限性。
Proxy参数与说明
参数
捕捉器函数
来称呼这些属性对
proxy
进行操作时,如果handler
对象中存在相应的捕捉器函数
则运行这个函数,如果不存在则直接对target
进行处理。在JavaScript中对于对象的大部分操作都存在
内部方法
,它是最底层的工作方式。例如对数据读取时底层会调用[[Get]]
,写入的时底层会调用[[Set]]
。我们不能直接通过方法名调用它,而Proxy
的代理配置
中的捕捉器函数
则可以拦截这些内部方法的调用。内部方法与捕捉器函数
下表描述了
内部方法
与捕捉器函数
的对应关系:[[Get]]
get
target
,property
,recevier
any
[[Set]]
set
target
,property
,value
,recevier
boolean
表示操作是否[[HasProperty]]
has
target
,property
boolean
[[Delete]]
deleteProperty
target
,property
boolean
表示操作是否[[Call]]
apply
target
,thisArg
,argumentsList
any
[[Construct]]
construct
target
,argumentsList
,newTarget
object
[[GetPrototypeOf]]
getPrototypeOf
target
object
或null
[[SetPrototypeOf]]
setPrototypeOf
target
,prototype
boolean
表示操作是否[[IsExtensible]]
isExtensible
target
boolean
[[PreventExtensions]]
preventExtensions
target
boolean
表示操作是否[[DefineOwnProperty]]
defineProperty
target
,property
,descriptor
boolean
表示操作是否ObjectdefineProperties
[[GetOwnProperty]]
getOwnPropertyDescriptor
target
,property
object
或undefined
for...in
Object.keys/values/entries
[[OwnPropertyKeys]]
ownKeys
target
object
.Object.getOwnPropertySymbols
for...in
Object.keys/values/entries
捕捉器函数参数说明
target
是目标对象,被作为第一个参数传递给new Proxy
property
将被设置或获取的属性名或Symbol
value
要设置的新的属性值recevier
最初被调用的对象。通常是proxy
本身,但是可能以其他方式被间接地调用(因此不一定是proxy
本身,后面我会说明)thisArg
被调用时的上下文对象argumentsList
被调用时的参数数组newTarget
最初被调用的构造函数descriptor
待定义或修改的属性的描述符这里我们重点讲一下捕捉器函数参数的
recevier
和newTarget
其他参数就不一一介绍,基本上一看就懂了。改造console.log
在
Proxy
的捕捉器函数
中使用console.log
很容易造成死循环,因为如果console.log(poxy)
时会读取Proxy
的属性,可能会经过捕捉器函数
,经过捕捉器函数
再次console.log(poxy)
。为了方便调试,我这里改造了以下console.log
。recevier与被代理方法上的this
recevier
最初被调用的对象,什么意思呢,就是谁调用的Proxy
经过捕捉器函数
那么它就是谁。看下方实例说明上方示例清晰的说明了
recevier
,就是当调用proxy
对象时调用者是谁,其实与function
中this
的机制是一致的。newTarget参数
newTarget
最初被调用的构造函数,在es6
中添加了class对象的支持,而newTarget
也就是主要识别类中继承关系的对象,比如看下方例子通过上面的例子我们可以比较清晰的知道最初被调用的构造函数的意思了,就是当外部使用
new Type()
时,无论是父类还是当前类construct捕捉器函数
的newTarget
参数都是指向这个Type
。大家注意到上方的construct捕捉器函数
内部实现中添加了设置原型,这里涉及到new
关键字,我们先讲讲new
和super
的内部工作原理当用户使用
new
关键字时class
原型的对象class
构建函数的this
指向上一步创建的对象,并执行super()
函数调用,将当前this
指向父类构造函数并执行super()
函数调用,则再次执行上一步super()
执行完成,如果没有返回对象则默认返回this
super()
执行的结果设置为当前构造函数的this
class
构造函数执行完成,如果没有返回对象则默认返回this
所以当我们不指定原型的情况下,上方的代码就会丢失所有子类的
原型
,原型
始终指向最顶级父类,因为super
时也会调用construct捕捉器函数
,这时new
创建一个原型指向当前class
原型的对象,并在返回时将子类的this
改变为刚刚创建的对象,所以子类的this
原型就只有父类的了。上面所使用的方法可以正常一切操作,但是这个实例终究是父级直接构造出来的,所以在构造方法中new.target
是指向父类构造方法的,如果使用console.log
打印出来会发现这个实例是Animal
对象, 可能有些同学会想着这样优化,比如:但是很遗憾
class
的构造函数加了限制,在class
构造期间会通过new.target检查当前是否是通过new
关键字调用,class
仅允许new
关键字调用, 直接通过函数式调用会报错,所以这种方法也无效,目前我没找到其他方法,如果各位大神有方法麻烦评论区贴一下谢谢了。有个最新的对象可以解决这个问题就是Reflect
这一块我们后面再整体讲一讲。代理具有私有属性的对象
类属性在默认情况下是公共的,可以被外部类检测或修改。在ES2020 实验草案 中,增加了定义私有类字段的能力,写法是使用一个
#
作为前缀。我们将上面的示例改造成类写法,先改造Animal
对象如下:上面代码直接运行报错了,为什么呢,我们通过recevier与被代理方法上的this得知在运行
animalProxy.getName()
时getName
方法的this
是指向animalProxy
的,而私有成员
是不允许外部访问的,访问时会直接报错,我们需要将this
改成正确的指向,如下:代理具有内部插槽的内建对象
有许多的
内建对象
比如Map
,Set
,Date
,Promise
都使用了内部插槽
,内部插槽
类似于上面的对象的私有属性,不允许外部访问,所以当代理没做处理时,直接代理他们会发生错误例如:在上方访问时
this
都是指向Proxy
的,而内部插槽只允许内部访问,Proxy
中没有这个内部插槽属性,所以只能失败,要处理这个问题可以像代理具有私有属性的对象中一样的方式处理,将function
的this
绑定,这样访问时就能正确的找到内部插槽了。ownKeys捕捉器函数
可能有些同学会想,为什么要把
ownKeys捕捉器
单独拎出来说呢,这不是一看就会的吗?别着急,大家往下看,里面还是有一个需要注意的知识点的。我们看这样一个例子:不错一切都预期运行,这时候产品过来加了个需求,根据身份证的前两位自动识别当前用户所在的省份,脑袋瓜子一转,直接在代理处识别添加不就好了,我们来改一下代码
可以看到对代理的附加属性直接访问是正常的,但是使用
Object.keys
获取属性列表的时候只能列出user
对象原有的属性,问题出在哪里了呢?这是因为
Object.keys
会对每个属性调用内部方法[[GetOwnProperty]]
获取它的属性描述符
,返回自身带有enumerable(可枚举)
的非Symbol
的key
。enumerable
是从对象的属性的描述符
中获取的,在上面的例子中province
没有属性的描述符
也就没有enumerable
属性了,所以province
会被忽略要解决这个问题就需要为
province
添加属性描述符,而通过我们上面内部方法与捕捉器函数表知道[[GetOwnProperty]]
获取时会通过getOwnPropertyDescriptor
捕捉器函数获取,我们加个这个捕捉器函数就可以解决了。注意
configurable
必须为true
,因为如果是不可配置的,Proxy
会阻止你为该属性的描述符代理。Reflect
在上文newTarget参数中我们使用了不完美的
construct捕捉器
处理函数,在创建子类时会多次new
父类对象,而且最终传出的也是顶级父类的对象,在console.log
时可以看出。其实Proxy
有一个最佳搭档,可以完美处理,那就是Reflect。Reflect
是一个内置的对象
,它提供拦截 JavaScript 操作的方法。这些方法与Proxy捕捉器
的方法相同。所有Proxy捕捉器
都有对应的Reflect
方法,而且Reflect
不是一个函数对象,因此它是不可构造的,我们可以像使用Math
使用他们比如Reflect.get(...)
,除了与Proxy捕捉器
一一对应外,Reflect
方法与Object
方法也有大部分重合,大家可以通过这里,比较 Reflect 和 Object 方法。下表描述了
Reflect
与捕捉器函数
的对应关系,而对应的Reflect
参数与捕捉器函数
大部分,参考内部方法与捕捉器函数get
target
,property
,recevier
set
target
,property
,value
,recevier
Boolean
值表明是否成功设置属性。has
target
,property
Boolean
类型的对象指示是否存在此属性。deleteProperty
target
,property
Boolean
值表明该属性是否被成功删除apply
target
,thisArg
,argumentsList
this
值的给定的函数后返回的结果。construct
target
,argumentsList
,newTarget
target
(如果newTarget
存在,则为newTarget
)为原型,调用target
函数为构造函数,argumentList
为其初始化参数的对象实例。getPrototypeOf
target
null
。setPrototypeOf
target
,prototype
Boolean
值表明是否原型已经成功设置。isExtensible
target
Boolean
值表明该对象是否可扩展preventExtensions
target
Boolean
值表明目标对象是否成功被设置为不可扩展getOwnPropertyDescriptor
target
,property
undefined
。ownKeys
target
Array
。Reflect的recevier参数
当使用
Reflect.get
或者Reflect.set
方法时会有可选参数recevier
传入,这个参数时使用getter
或者setter
时可以改变this
指向使用的,如果不使用Reflect
时我们是没办法改变getter
或者setter
的this
指向的因为他们不是一个方法,参考下方示例:Reflect的newTarget参数
当使用
Reflect.construct
时会有一个可选参数newTarget
参数可以传入,Reflect.construct
是一个能够new Class
的方法实现,比如new User('bill')
与Reflect.construct(User, ['bill'])
是一致的,而newTarget
可以改变创建出来的对象的原型,在es5
中能够用Object.create
实现,但是有略微的区别,在构造方法中new.target
可以查看到当前构造方法,如果使用es5
实现的话这个对象是undefined
因为不是通过new
创建的,使用Reflect.construct
则没有这个问题 参考下方两种实现方式construct捕捉器
在newTarget参数中我们实现了不完美的
construct捕捉器
,而通过阅读Reflect,我们知道了一个能够完美契合我们想要的能够实现的方案,那就是Reflect.construct
不仅能够识别new.target
,也能够处理多是创建对象问题,我们改造一下实现,示例如下代理setter、getter函数
我们通过阅读recevier与被代理方法上的this知道了
recevier
的指向,接下来请思考这样一段代码如果你运行上方代码会发现打印顺序依次是
动物,动物,猪,动物
,使用getName
通过方法访问时是没问题的,因为代理拿到了getName
的实现,然后通过当前对象访问,所以this
是当前谁调用就是谁,但是通过getter
调用时,在通过target[key]
时就已经调用了方法实现,所以this
始终是指向当前代理的对象target
,想要修正这里就得通过代理内的捕捉器
入手,修正this
的对象,而recevier
就是指向当前调用者的,但是getter
不像成员方法可以直接通过bind、call、apply
能够修正this
,这时候我们就要借助Reflect.get
方法了。setter
的原理也是一样的这里就不作多讲了,参考下方Proxy与Reflect的结合
因为
Reflect
与Proxy
的捕捉器
都有对应的方法,所以大部分情况下我们都能直接使用Reflect
的API
来对Proxy
的操作相结合。我们能专注Proxy
要执行的业务比如下方代码Proxy.revocable撤销代理
假如有这么一个业务,我们在做一个商城系统,产品要求跟踪用户的商品内操作的具体踪迹,比如展开了商品详情,点击播放了商品的视频等等,为了与具体业务脱耦,使用
Proxy
是一个不错的选择于是我们写了下面这段代码我们编写了上方,不错很完美,但是后期一堆客户反应不希望自己的行踪被跟踪,产品又要求我们改方案,用户可以在设置中要求不跟踪,不能直接重启刷新页面,也不能让缓存中的商品对象重新加载这时候,如果让新的商品不被代理很简单只要加个判断就行了,但是旧数据也不能重新加载,那就只能撤销代理了,接下来我们介绍一下新的API
Proxy.revocable(target, handler)方法可以用来创建一个可撤销的代理对象。该方法的参数与
new Proxy(target, handler)
一样,第一个参数传入要代理的对象,第二个参数传入捕捉器
。该方法返回一个对象,这个对象的proxy
返回target
的代理对象,revoke
返回撤销代理的方法,具体使用如下接下来我们改进一下我们的跟踪代码,如下
还有一个问题,我们看到当
revoke()
撤销代理后我们并没有返回代理前的commodity
对象,这该怎么办呢,怎么从代理处拿取代理前的对象呢,我认为比较好的有两种方案,我们往下看。通过代理获取被代理对象
通过代理处拿取代理前的对,我认为有两种比较好的方案我分别介绍一下。
1:Proxy.revocable撤销代理中实例看到,我们既然添加了
proxy
与revoke
的WeakMap
对象,为什么不多添加一份proxy
与target
的对象呢,说说干就干2:与第一种方案不同,第二种方案是直接在代理的
get捕捉器
中加入逻辑处理,既然我们能够拦截get
,那我们就能够在里面添加一些我们track-commodity.js
的内置逻辑,就是当get
某个key
时我们就返回代理的原始对象,当然这个key
不能和业务中使用到的commodity
的key
冲突,而且要确保只有内部使用,所以我们需要使用到Symbol,只要不导出用户就拿不到这个key
就都解决了,参考下方代码Proxy的局限性
代理提供了一种独特的方法,可以在调整现有对象的行为,但是它并不完美,有一定的局限性。
代理私有属性
我们在代理具有私有属性的对象时介绍了如何避开
this
是当前代理无法访问私有属性的问题,但是这里也有一定的问题,因为一个对象里肯定不止只有访问私有属性的方法,如果有访问自身非私有属性时,这里的处理方式有一定的问题,比如下方代码因为只要是
function
都会执行bind
绑定当前被代理的对象animal
,所以当pig
通过原型继承了animalProxy
之后this
访问的都是animal
,还有,这意味着我们要熟悉被代理对象内的api
,通过识别是否是私有属性访问才绑定this
,需要了解被代理对象的api
。还有一个问题是私有属性只允许自身访问,在没有代理的帮助下上方的pig.getName()
会出错TypeError
,而通过bind
之后就可以正常访问,这一块要看具体业务,不过还是建议跟没代理时保持一致,这里处理比较简单,在知道使用私有属性api
之后,只要识别当前访问对象是否是原对象的代理即可。具体处理代码下方所示target !== Proxy
代理跟原对象肯定是不同的对象,所以当我们使用原对象进行管理后代理却无法进行正确管理,比如下方代理做了一个所有用户实例的集中管理:
所以在开发中这类问题需要特别注意,在开发时假如对一个对象做代理时,对代理的所有管理也需要再进行一层代理,原对象对原对象,代理对代理,比如上方这个实例可以通过下方代码改进
Proxy
就介绍到这里了,本文介绍了Proxy
大部分要注意的问题以及用法。The text was updated successfully, but these errors were encountered: