当前位置: 首页 > 科技观察

为什么Proxy必须和Reflect一起使用?

时间:2023-03-15 19:47:25 科技观察

简介EcmaScript2015引入了两个新的内置模块,Proxy[1]和Reflect[2]。我们可以使用Proxy和Reflect来实现对对象的代理劫持操作,类似于Es5中Object.defineProperty\(\)[3]的效果,但是Reflect&Proxy远比它强大。大多数开发者都知道Es6中这两个新的内置模块,但你可能不知道为什么Proxy必须与Reflect一起使用。这里,文章通过几个通俗易懂的例子来描述它们之间的互补关系。Pre-knowledgeProxy[4]代理,它内置了一系列“陷阱”来为一个对象创建一个代理,从而实现对基本操作(如属性查找、赋值、枚举、函数调用等)。Reflect[5]反射,它提供了拦截JavaScript操作的方法。这些方法与Proxy[6]的方法相同。简单来说,我们可以通过Proxy为原对象创建一个代理对象,这样我们就可以在代理对象中使用Reflect来拦截JavaScript的原操作。如果您还不知道,请前往MDN了解他们的知识。毕竟大名鼎鼎的VueJs/Core中的核心响应式模块就是基于这两个API实现的。第一个单独使用Proxy的例子,我们单独使用Proxy来做一道简单的开胃菜:constobj={name:'wang.haoyu',};constproxy=newProxy(obj,{//gettrapintarget,原始对象key为访问的属性名get(target,key){console.log('Hijackyourdataaccess'+key);returntarget[key]},});proxy.name//hijackyou数据访问名->wang.haoyu复制代码看起来很简单吧?我们通过Proxy创建了一个基于obj对象的代理,并在Proxy中声明了一个get陷阱。当访问proxy.name的时候,对应的gettrap实际上被触发了,它会执行gettrap中的逻辑,同时执行对应trap中的逻辑,最后返回对应的target[key],也就是所谓wang.haoyu。Proxy中接收器上方的Demo中的所有内容看起来都很流畅,对吧?细心的同学看了Proxy的MDN文档后可能会发现Proxy中的gettrap中多了一个参数receiver。那么这里的receiver是什么意思呢?大部分同学会理解为代理对象,但这并不全面。接下来,我们也以一个简单的例子作为出发点:constobj={name:'wang.haoyu',};constproxy=newProxy(obj,{//gettrap中的target表示原始对象key表示访问get(target,key,receiver)的属性名{console.log(receiver===proxy);returntarget[key];},});//日志:trueproxy.name;复制代码在上面的例子中,我们在Proxy实例对象的gettrap上接收到receiver参数。同时我们打印console.log(receiver===proxy);在陷阱里;它将打印true,表明接收者确实等于代理对象。所以receiver确实可以代表一个代理对象,但这只是receiver代表的一种情况。接下来我们再看一个例子:constparent={getvalue(){return'19Qingfeng';},};constproxy=newProxy(parent,{//gettrap中的target表示原始对象key表示要访问的属性nameget(target,key,receiver){console.log(receiver===proxy);returntarget[钥匙];},});constobj={name:'wang.haoyu',};//setobj继承和父代理对象proxyObject.setPrototypeOf(obj,proxy);//log:falseobj.value复制代码关于get的“屏蔽”作用/setpropertyaccessor出现在原型上,我在这篇文章[7]中详细阐述。我不会在这里解释。我们可以看到在上面的代码中,我还打印了console.log(receiver===proxy);关于代理对象的获取陷阱;结果是错误的。那你可以想想这里的receiver是什么?其实这也是proxy中get陷阱的第三个receiver的意思。就是把正确的指针传递给调用者,可以看下面的代码:...constproxy=newProxy(parent,{//gettrap中的target表示原始对象key表示访问get的属性名(target,key,receiver){-console.log(receiver===proxy)//log:false+console.log(receiver===obj)//log:truereturntarget[key];},});...copy代码其实很简单。gettrap中receiver存在的意义就是正确传递trap中的context。说到属性访问,别忘了gettrap也会触发相应的属性访问器,即所谓的get访问器方法。我们可以清楚的看到上面的receiver代表的是继承和Proxy的对象,也就是obj。看到这里,我们就明白Proxy中get陷阱的receiver不仅代表了Proxy代理对象本身,还可能代表继承自Proxy的对象。其实本质上是为了保证trap函数中调用者正确的上下文访问,比如这里的receiver指向obj。当然,您不应该将revceiver与get陷阱中的this混淆。陷阱中的this关键字表示代理的处理程序对象。例如:constparent={getvalue(){return'19Qingfeng';},};consthandler={get(target,key,receiver){console.log(this===handler);//日志:trueconsole.log(receiver===obj);//日志:true返回目标[key];},};constproxy=newProxy(parent,handler);constobj={name:'wang.haoyu',};//设置obj继承和父类的代理对象proxyObject.setPrototypeOf(obj,proxy);//log:falseobj.valuecopycodeReceiverinReflect清空Proxy中get陷阱的receiver后,趁热打铁Let's说说Reflect反射API中get陷阱的接收者。我们知道,在Proxy(下面以gettrap为例)中,第三个参数receiver表示代理对象本身或者继承自代理对象的对象,表示触发trap时的正确上下文。constparent={name:'19Qingfeng',getvalue(){returnthis.name;},};consthandler={get(target,key,receiver){returnReflect.get(target,key);//这里相当于returntarget[key]},};constproxy=newProxy(parent,handler);constobj={name:'wang.haoyu',};//设置obj继承父对象的代理对象proxyObject.setPrototypeOf(obj,proxy);//log:falseconsole.log(obj.value);复制代码我们稍微分析一下上面的代码:当我们调用obj.value的时候,因为obj本身是没有value属性的。值属性访问操作符存在于它继承的代理对象中,所以会产生屏蔽效应。这会触发代理上的getvalue()属性访问操作。同时由于访问了代理上的value属性访问器,此时会触发get陷阱。进入陷阱时,目标是源对象,也就是parent,key是value。在陷阱中返回Reflect.get(target,key)等同于target[key]。此时,不知不觉中,这个点在get陷阱中被偷偷修改了!!原来调用者的obj在trap中被修改为对应的target,也就是parent。自然就打印出了对应的parent[value],就是19Qingfeng。这显然不是我们预期的结果。我在访问obj.value的时候,希望自己对应的name属性应该正确输出,也就是所谓的obj.value=>wang.haoyu。然后,Relfect中gettrap的接收者将发挥它的魔力。constparent={name:'19Qingfeng',getvalue(){returnthis.name;},};consthandler={get(target,key,receiver){-returnReflect.get(target,key);+returnReflect.get(target,key,receiver);},};constproxy=newProxy(parent,handler);constobj={name:'wang.haoyu',};//设置obj继承父对象的代理对象proxyObject.setPrototypeOf(obj,proxy);//log:wang.haoyuconsole.log(obj.value);复制代码上面代码的原理其实很简单:首先,我们之前提到Proxy中get陷阱的receiver不仅会表示代理对象本身还可能代表一个继承自代理对象的对象,这需要与调用者区分开来。这里很明显指向的是被继承和代理对象的obj。其次,我们在Proxy中传入receiver,也就是obj,作为Reflect中get陷阱中的第三个参数,调用时会修改this点。你可以简单的把Reflect.get(target,key,receiver)理解为target[key].call(receiver),不过这??是一段伪代码,但是这样理解可能会更好。相信看到这里你已经明白Relfect中的receiver是什么意思了。是的,它可以修改传入接收者对象的属性访问中的this点。image.png总结相信大家已经明白为什么Proxy一定要和Reflect一起使用了。这正是为什么在触发代理对象劫持时保证指向正确的this上下文的原因。我们再稍微回顾一下,对于gettrap(当然set和其他涉及receiver的trap也是如此):Proxy中接受的Receiver形参代表了代理对象本身或者继承自代理对象的对象。Reflect中传入Receiver的实参,意思是在执行原操作时修改this点。文章到此结束,至于为什么突然提到Proxy&Reflect的话题。其实笔者最近在看Vue/corejs的源码内容,恰好在Proxy&Reflect中被广泛使用,所以产生了这篇文章。至于为什么Proxy一定要和Reflect一起使用,结合VueJs中响应式模块的依赖集合会更好理解。不过为了照顾不熟悉VueJs的同学,这里就不展开了。当然最近在看VueJs的过程中也尝试写了一些阶段性的总结文章。这个过程将在本文后面详细解释。感兴趣的同学可以继续关注我的最新动态。最后谢谢各位小伙伴。一起努力吧~