首发于 JavaScript笔录

Proxy和Reflect的要注意的问题与局限性

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 Proxy被用于许多库和浏览器框架上,例如vue3就是使用 Proxy来实现数据响应式的。本文带你了解 Proxy的用法与局限性。

Proxy参数与说明

参数

proxy进行操作时,如果handler对象中存在相应的捕捉器函数则运行这个函数,如果不存在则直接对target进行处理。
在JavaScript中对于对象的大部分操作都存在内部方法,它是最底层的工作方式。例如对数据读取时底层会调用[[Get]],写入的时底层会调用[[Set]]。我们不能直接通过方法名调用它,而Proxy代理配置中的捕捉器函数则可以拦截这些内部方法的调用。

内部方法与捕捉器函数

下表描述了内部方法捕捉器函数的对应关系:

捕捉器函数参数说明

这里我们重点讲一下捕捉器函数参数的receviernewTarget其他参数就不一一介绍,基本上一看就懂了。

改造console.log

Proxy捕捉器函数中使用console.log很容易造成死循环,因为如果console.log(poxy)时会读取Proxy的属性,可能会经过捕捉器函数,经过捕捉器函数再次console.log(poxy)。为了方便调试,我这里改造了以下console.log

recevier与被代理方法上的this

recevier最初被调用的对象,什么意思呢,就是谁调用的Proxy经过捕捉器函数那么它就是谁。看下方实例说明

上方示例清晰的说明了recevier,就是当调用proxy对象时调用者是谁,其实与functionthis的机制是一致的。

newTarget参数

newTarget 最初被调用的构造函数,在es6中添加了 class对象的支持,而newTarget也就是主要识别类中继承关系的对象,比如看下方例子

通过上面的例子我们可以比较清晰的知道最初被调用的构造函数的意思了,就是当外部使用new Type()时,无论是父类还是当前类 construct捕捉器函数newTarget参数都是指向这个Type。大家注意到上方的construct捕捉器函数内部实现中添加了设置原型,这里涉及到new关键字,我们先讲讲newsuper的内部工作原理 当用户使用new关键字时

所以当我们不指定原型的情况下,上方的代码就会丢失所有子类的原型,原型始终指向最顶级父类,因为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改成正确的指向,如下:

代理具有内部插槽的内建对象

有许多的内建对象比如MapSetDatePromise都使用了内部插槽内部插槽类似于上面的对象的私有属性,不允许外部访问,所以当代理没做处理时,直接代理他们会发生错误例如:

在上方访问时this都是指向Proxy的,而内部插槽只允许内部访问,Proxy中没有这个内部插槽属性,所以只能失败,要处理这个问题可以像 代理具有私有属性的对象中一样的方式处理,将functionthis绑定,这样访问时就能正确的找到内部插槽了。

ownKeys捕捉器函数

可能有些同学会想,为什么要把ownKeys捕捉器单独拎出来说呢,这不是一看就会的吗?别着急,大家往下看,里面还是有一个需要注意的知识点的。我们看这样一个例子:

不错一切都预期运行,这时候产品过来加了个需求,根据身份证的前两位自动识别当前用户所在的省份,脑袋瓜子一转,直接在代理处识别添加不就好了,我们来改一下代码

可以看到对代理的附加属性直接访问是正常的,但是使用Object.keys获取属性列表的时候只能列出user对象原有的属性,问题出在哪里了呢?
这是因为Object.keys会对每个属性调用内部方法[[GetOwnProperty]]获取它的属性描述符,返回自身带有enumerable(可枚举)的非Symbolkeyenumerable是从对象的属性的描述符中获取的,在上面的例子中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参数与捕捉器函数大部分,参考 内部方法与捕捉器函数

Reflect的recevier参数

当使用Reflect.get或者Reflect.set方法时会有可选参数recevier传入,这个参数时使用getter或者setter时可以改变this指向使用的,如果不使用Reflect时我们是没办法改变getter或者setterthis指向的因为他们不是一个方法,参考下方示例:

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的结合

因为ReflectProxy捕捉器都有对应的方法,所以大部分情况下我们都能直接使用ReflectAPI来对Proxy的操作相结合。我们能专注Proxy要执行的业务比如下方代码

Proxy.revocable撤销代理

假如有这么一个业务,我们在做一个商城系统,产品要求跟踪用户的商品内操作的具体踪迹,比如展开了商品详情,点击播放了商品的视频等等,为了与具体业务脱耦,使用Proxy是一个不错的选择于是我们写了下面这段代码

我们编写了上方,不错很完美,但是后期一堆客户反应不希望自己的行踪被跟踪,产品又要求我们改方案,用户可以在设置中要求不跟踪,不能直接重启刷新页面,也不能让缓存中的商品对象重新加载这时候,如果让新的商品不被代理很简单只要加个判断就行了,但是旧数据也不能重新加载,那就只能撤销代理了,接下来我们介绍一下新的API
Proxy.revocable(target, handler)方法可以用来创建一个可撤销的代理对象。该方法的参数与new Proxy(target, handler)一样,第一个参数传入要代理的对象,第二个参数传入捕捉器。该方法返回一个对象,这个对象的proxy返回target的代理对象,revoke返回撤销代理的方法,具体使用如下

接下来我们改进一下我们的跟踪代码,如下

还有一个问题,我们看到当revoke()撤销代理后我们并没有返回代理前的commodity对象,这该怎么办呢,怎么从代理处拿取代理前的对象呢,我认为比较好的有两种方案,我们往下看。

通过代理获取被代理对象

通过代理处拿取代理前的对,我认为有两种比较好的方案我分别介绍一下。
1: Proxy.revocable撤销代理中实例看到,我们既然添加了proxyrevokeWeakMap对象,为什么不多添加一份proxytarget的对象呢,说说干就干

2:与第一种方案不同,第二种方案是直接在代理的get捕捉器中加入逻辑处理,既然我们能够拦截get,那我们就能够在里面添加一些我们track-commodity.js的内置逻辑,就是当get某个key时我们就返回代理的原始对象,当然这个key不能和业务中使用到的commoditykey冲突,而且要确保只有内部使用,所以我们需要使用到 Symbol,只要不导出用户就拿不到这个key就都解决了,参考下方代码

Proxy的局限性

代理提供了一种独特的方法,可以在调整现有对象的行为,但是它并不完美,有一定的局限性。

代理私有属性

我们在 代理具有私有属性的对象时介绍了如何避开this是当前代理无法访问私有属性的问题,但是这里也有一定的问题,因为一个对象里肯定不止只有访问私有属性的方法,如果有访问自身非私有属性时,这里的处理方式有一定的问题,比如下方代码

因为只要是function都会执行bind绑定当前被代理的对象animal,所以当pig通过原型继承了animalProxy之后this访问的都是animal,还有,这意味着我们要熟悉被代理对象内的api,通过识别是否是私有属性访问才绑定this,需要了解被代理对象的api。还有一个问题是私有属性只允许自身访问,在没有代理的帮助下上方的pig.getName()会出错TypeError,而通过bind之后就可以正常访问,这一块要看具体业务,不过还是建议跟没代理时保持一致,这里处理比较简单,在知道使用私有属性api之后,只要识别当前访问对象是否是原对象的代理即可。具体处理代码下方所示

target !== Proxy

代理跟原对象肯定是不同的对象,所以当我们使用原对象进行管理后代理却无法进行正确管理,比如下方代理做了一个所有用户实例的集中管理:

所以在开发中这类问题需要特别注意,在开发时假如对一个对象做代理时,对代理的所有管理也需要再进行一层代理,原对象对原对象,代理对代理,比如上方这个实例可以通过下方代码改进

Proxy就介绍到这里了,本文介绍了Proxy大部分要注意的问题以及用法。

两个鬼故事怎么给自己起笔名借钱利息三维制图软件励志小故事上门女婿叶辰驾校从报名起几年有效男孩子起名起名生辰八字中国医生在线观看中国最冷小镇迎来今秋首场大雪悲剧的诞生在线互联网教育公司起名灵蛇爱泰剧2021冷箭演员表姓易的女孩起名大全花粉少女注意报取名起名用锦起名字好给翡翠店铺起名的词叶无道徐灵儿小说全文免费最新更新窗饰遮阳公司起名男孩起名翁姓2019年猪宝宝起名宜忌用字宝宝起名的 五行0除以任何数都得0对吗?金字旁起名的字有哪些适合猪宝宝起名常用的字imca泡芙小姐的金鱼缸男孩起名博文伦敦时间与北京时间少年生前被连续抽血16次?多部门介入两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”淀粉肠小王子日销售额涨超10倍高中生被打伤下体休学 邯郸通报单亲妈妈陷入热恋 14岁儿子报警何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言张家界的山上“长”满了韩国人?男孩8年未见母亲被告知被遗忘中国拥有亿元资产的家庭达13.3万户19岁小伙救下5人后溺亡 多方发声315晚会后胖东来又人满为患了张立群任西安交通大学校长“重生之我在北大当嫡校长”男子被猫抓伤后确诊“猫抓病”测试车高速逃费 小米:已补缴周杰伦一审败诉网易网友洛杉矶偶遇贾玲今日春分倪萍分享减重40斤方法七年后宇文玥被薅头发捞上岸许家印被限制高消费萧美琴窜访捷克 外交部回应联合利华开始重组专访95后高颜值猪保姆胖东来员工每周单休无小长假男子被流浪猫绊倒 投喂者赔24万小米汽车超级工厂正式揭幕黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发当地回应沈阳致3死车祸车主疑毒驾恒大被罚41.75亿到底怎么缴妈妈回应孩子在校撞护栏坠楼外国人感慨凌晨的中国很安全杨倩无缘巴黎奥运校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变王树国卸任西安交大校长 师生送别手机成瘾是影响睡眠质量重要因素国产伟哥去年销售近13亿阿根廷将发行1万与2万面值的纸币兔狲“狲大娘”因病死亡遭遇山火的松茸之乡“开封王婆”爆火:促成四五十对奥巴马现身唐宁街 黑色着装引猜测考生莫言也上北大硕士复试名单了德国打算提及普京时仅用姓名天水麻辣烫把捣辣椒大爷累坏了

两个鬼故事 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化