JavaScript中的ES6 Proxy的具体使用
场景
就算只是扮演,也会成为真实的自我的一部分。对人类的精神来说,真实和虚假其实并没有明显的界限。入戏太深不是一件好事,但对于你来说并不成立,因为戏中的你才是真正符合你的身份的你。如今的你是真实的,就算一开始你只是在模仿着这种形象,现在的你也已经成为了这种形象。无论如何,你也不可能再回到过去了。
Proxy代理,在JavaScript似乎很陌生,却又在生活中无处不在。或许有人在学习ES6的时候有所涉猎,但却并未真正了解它的使用场景,平时在写业务代码时也不会用到这个特性。
相比于文绉绉的定义内容,想必我们更希望了解它的使用场景,使其在真正的生产环境发挥强大的作用,而不仅仅是作为一个新的特性--然后,实际中完全没有用到!
- 为函数添加特定的功能
- 代理对象的访问
- 作为胶水桥接不同结构的对象
- 监视对象的变化
- 还有更多。。。
如果你还没有了解过Proxy特性,可以先去MDNProxy上查看基本概念及使用。
为函数添加特定的功能
下面是一个为异步函数自动添加超时功能的高阶函数,我们来看一下它有什么问题
/** *为异步函数添加自动超时功能 *@paramtimeout超时时间 *@paramaction异步函数 *@returns包装后的异步函数 */ functionasyncTimeout(timeout,action){ returnfunction(...args){ returnPromise.race([ Reflect.apply(action,this,args), wait(timeout).then(Promise.reject), ]) } }
一般而言,上面的代码足以胜任,但问题就在这里,不一般的情况--函数上面包含自定义属性呢?
众所周知,JavaScript中的函数是一等公民,即函数可以被传递,被返回,以及,被添加属性!
例如下面这个简单的函数get,其上有着_name这个属性
constget=asynci=>i get._name='get'
一旦使用上面的asyncTimeout函数包裹之后,问题便会出现,返回的函数中_name属性不见了。这是当然的,毕竟实际上返回的是一个匿名函数。那么,如何才能让返回的函数能够拥有传入函数参数上的所有自定义属性呢?
一种方式是复制参数函数上的所有属性,但这点实现起来其实并不容易,真的不容易,不信你可以看看Lodash的clone函数。那么,有没有一种更简单的方式呢?答案就是Proxy,它可以代理对象的指定操作,除此之外,其他的一切都指向原对象。
下面是Proxy实现的asyncTimeout函数
/** *为异步函数添加自动超时功能 *@paramtimeout超时时间 *@paramaction异步函数 *@returns包装后的异步函数 */ functionasyncTimeout(timeout,action){ returnnewProxy(action,{ apply(_,_this,args){ returnPromise.race([ Reflect.apply(_,_this,args), wait(timeout).then(Promise.reject), ]) }, }) }
测试一下,是可以正常调用与访问其上的属性的
;(async()=>{ console.log(awaitget(1)) console.log(get._name) })()
好了,这便是吾辈最常用的一种方式了--封装高阶函数,为函数添加某些功能。
代理对象的访问
下面是一段代码,用以在页面上展示从后台获取的数据,如果字段没有值则默认展示''
模拟一个获取列表的异步请求
asyncfunctionlist(){ //此处仅为构造列表 classPerson{ constructor({id,name,age,sex,address}={}){ this.id=id this.name=name this.age=age this.sex=sex this.address=address } } return[ newPerson({id:1,name:'琉璃'}), newPerson({id:2,age:17}), newPerson({id:3,sex:false}), newPerson({id:4,address:'幻想乡'}), ] }
尝试直接通过解构为属性赋予默认值,并在默认值实现这个功能
;(async()=>{ //为所有为赋值属性都赋予默认值'' constpersons=(awaitlist()).map( ({id='',name='',age='',sex='',address=''})=>({ id, name, age, sex, address, }), ) console.log(persons) })()
下面让我们写得更通用一些
functionwarp(obj){ constresult=obj for(constkofReflect.ownKeys(obj)){ constv=Reflect.get(obj,k) result[k]=v===undefined?'':v } returnobj } ;(async()=>{ //为所有为赋值属性都赋予默认值'' constpersons=(awaitlist()).map(warp) console.log(persons) })()
暂且先看一下这里的warp函数有什么问题?
这里是答案的分割线
- 所有属性需要预定义,不能运行时决定
- 没有指向原对象,后续的修改会造成麻烦
吾辈先解释一下这两个问题
- 所有属性需要预定义,不能运行时决定
如果调用了list[0].a会发生什么呢?是的,依旧会是undefined,因为Reflect.ownKeys也不能找到没有定义的属性(真*undefined),因此导致访问未定义的属性仍然会是undefined而非期望的默认值。
- 没有指向原对象,后续的修改会造成麻烦
如果我们此时修改对象的一个属性,那么会影响到原本的属性么?不会,因为warp返回的对象已经是全新的了,和原对象没有什么联系。所以,当你修改时当然不会影响到原对象。
Pass:我们当然可以直接修改原对象,但这很明显不太符合我们的期望:显示时展示默认值''--这并不意味着我们愿意在其他操作时需要'',否则我们还要再转换一遍。(例如发送编辑后的数据到后台)
这个时候Proxy也可以派上用场,使用Proxy实现warp函数
functionwarp(obj){ constresult=newProxy(obj,{ get(_,k){ constv=Reflect.get(_,k) if(v!==undefined){ returnv } return'' }, }) returnresult }
现在,上面的那两个问题都解决了!
注:知名的GitHub库immer就使用了该特性实现了不可变状态树。
作为胶水桥接不同结构的对象
通过上面的例子我们可以知道,即便是未定义的属性,Proxy也能进行代理。这意味着,我们可以通过Proxy抹平相似对象之间结构的差异,以相同的方式处理类似的对象。
Pass:不同公司的项目中的同一个实体的结构不一定完全相同,但基本上类似,只是字段名不同罢了。所以使用Proxy实现胶水桥接不同结构的对象方便我们在不同公司使用我们的工具库!
嘛,开个玩笑,其实在同一个公司中不同的实体也会有类似的结构,也会需要相同的操作,最常见的应该是树结构数据。例如下面的菜单实体和系统权限实体就很相似,也需要相同的操作--树<=>列表相互转换。
思考一下如何在同一个函数中处理这两种树节点结构
/** *系统菜单 */ classSysMenu{ /** *构造函数 *@param{Number}id菜单id *@param{String}name显示的名称 *@param{Number}parent父级菜单id */ constructor(id,name,parent){ this.id=id this.name=name this.parent=parent } } /** *系统权限 */ classSysPermission{ /** *构造函数 *@param{String}uid系统唯一uuid *@param{String}label显示的菜单名 *@param{String}parentId父级权限uid */ constructor(uid,label,parentId){ this.uid=uid this.label=label this.parentId=parentId } }
下面让我们使用Proxy来抹平访问它们之间的差异
constsysMenuProxy={parentId:'parent'} constsysMenu=newProxy(newSysMenu(1,'rx',0),{ get(_,k){ if(Reflect.has(sysMenuProxy,k)){ returnReflect.get(_,Reflect.get(sysMenuProxy,k)) } returnReflect.get(_,k) }, }) console.log(sysMenu.id,sysMenu.name,sysMenu.parentId)//1'rx'0 constsysPermissionProxy={id:'uid',name:'label'} constsysPermission=newProxy(newSysPermission(1,'rx',0),{ get(_,k){ if(Reflect.has(sysPermissionProxy,k)){ returnReflect.get(_,Reflect.get(sysPermissionProxy,k)) } returnReflect.get(_,k) }, }) console.log(sysPermission.id,sysPermission.name,sysPermission.parentId)//1'rx'0
看起来似乎有点繁琐,让我们封装一下
/** *桥接对象不存在的字段 *@param{Object}map代理的字段映射Map *@returns{Function}转换一个对象为代理对象 */ functionbridge(map){ /** *为对象添加代理的函数 *@param{Object}obj任何对象 *@returns{Proxy}代理后的对象 */ returnfunction(obj){ returnnewProxy(obj,{ get(target,k){ //如果遇到被代理的属性则返回真实的属性 if(Reflect.has(map,k)){ returnReflect.get(target,Reflect.get(map,k)) } returnReflect.get(target,k) }, set(target,k,v){ //如果遇到被代理的属性则设置真实的属性 if(Reflect.has(map,k)){ Reflect.set(target,Reflect.get(map,k),v) returntrue } Reflect.set(target,k,v) returntrue }, }) } }
现在,我们可以用更简单的方式来做代理了。
constsysMenu=bridge({ parentId:'parent', })(newSysMenu(1,'rx',0)) console.log(sysMenu.id,sysMenu.name,sysMenu.parentId)//1'rx'0 constsysPermission=bridge({ id:'uid', name:'label', })(newSysPermission(1,'rx',0)) console.log(sysPermission.id,sysPermission.name,sysPermission.parentId)//1'rx'0
如果想看JavaScirpt如何处理树结构数据话,可以参考吾辈的JavaScript处理树数据结构
监视对象的变化
接下来,我们想想,平时是否有需要监视对象的变化,然后进行某些处理呢?
例如监视用户复选框选中项列表的变化并更新对应的需要发送到后台的id拼接字符串。
//模拟页面的复选框列表 consthobbyMap=newMap() .set(1,'小说') .set(2,'动画') .set(3,'电影') .set(4,'游戏') constuser={ id:1, //保存兴趣id的列表 hobbySet:newSet(), //发送到后台的兴趣id拼接后的字符串,以都好进行分割 hobby:'', } functiononClick(id){ user.hobbySet.has(id)?user.hobbySet.delete(id):user.hobbySet.add(id) } //模拟两次点击 onClick(1) onClick(2) console.log(user.hobby)//''
下面使用Proxy来完成hobbySet属性改变后hobby自动更新的操作
/** *深度监听指定对象属性的变化 *注:指定对象不能是原始类型,即不可变类型,而且对象本身的引用不能改变,最好使用const进行声明 *@paramobject需要监视的对象 *@paramcallback当代理对象发生改变时的回调函数,回调函数有三个参数,分别是对象,修改的key,修改的v *@returns返回源对象的一个代理 */ functionwatchObject(object,callback){ consthandler={ get(_,k){ try{ //注意:这里很关键,它为对象的字段也添加了代理 returnnewProxy(v,Reflect.get(_,k)) }catch(err){ returnReflect.get(_,k) } }, set(_,k,v){ callback(_,k,v) returnReflect.set(_,k,v) }, } returnnewProxy(object,handler) } //模拟页面的复选框列表 consthobbyMap=newMap() .set(1,'小说') .set(2,'动画') .set(3,'电影') .set(4,'游戏') constuser={ id:1, //保存兴趣id的列表 hobbySet:newSet(), //发送到后台的兴趣id拼接后的字符串,以都好进行分割 hobby:'', } constproxy=watchObject(user,(_,k,v)=>{ if(k==='hobbySet'){ _.hobby=[..._.hobbySet].join(',') } }) functiononClick(id){ proxy.hobbySet=proxy.hobbySet.has(id) ?proxy.hobbySet.delete(id) :proxy.hobbySet.add(id) } //模拟两次点击 onClick(1) onClick(2) //现在,user.hobby的值将会自动更新 console.log(user.hobby)//1,2
当然,这里实现的watchObject函数还非常非常非常简陋,如果有需要可以进行更深度/强大的监听,可以尝试自行实现一下啦!
缺点
说完了这些Proxy的使用场景,下面稍微来说一下它的缺点
运行环境必须要ES6支持
这是一个不大不小的问题,现代的浏览器基本上都支持ES6,但如果泥萌公司技术栈非常老旧的话(例如支持IE6),还是安心吃土吧#笑#这种公司不离职等着老死
不能直接代理一些需要this的对象
这个问题就比较麻烦了,任何需要this的对象,代理之后的行为可能会发生变化。例如Set对象
constproxy=newProxy(newSet([]),{}) proxy.add(1)//MethodSet.prototype.addcalledonincompatiblereceiver[objectObject]
是不是很奇怪,解决方案是把所有的get操作属性值为function的函数都手动绑定this
constproxy=newProxy(newSet([]),{ get(_,k){ constv=Reflect.get(_,k) //遇到Function都手动绑定一下this if(vinstanceofFunction){ returnv.bind(_) } returnv }, }) proxy.add(1)
总结
Proxy是个很强大的特性,能够让我们实现一些曾经难以实现的功能(所以这就是你不支持ES5的理由?#打),就连Vue3+都开始使用Proxy实现了,你还有什么理由在乎上古时期的IE而不用呢?(v^_^)v
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。