JavaScript的Proxy可以做哪些有意思的事儿
Proxy是什么
首先,我们要清楚,Proxy是什么意思,这个单词翻译过来,就是代理。
可以理解为,有一个很火的明星,开通了一个微博账号,这个账号非常活跃,回复粉丝、到处点赞之类的,但可能并不是真的由本人在维护的。
而是在背后有一个其他人or团队来运营,我们就可以称他们为代理人,因为他们发表的微博就代表了明星本人的意思。
P.S.强行举例子,因为本人不追星,只是猜测可能会有这样的运营团队
这个代入到JavaScript当中来,就可以理解为对对象或者函数的代理操作。
JavaScript中的Proxy
Proxy是ES6中提供的新的API,可以用来定义对象各种基本操作的自定义行为(在文档中被称为traps,我觉得可以理解为一个针对对象各种行为的钩子),拿它可以做很多有意思的事情,在我们需要对一些对象的行为进行控制时将变得非常有效。
Proxy的语法
创建一个Proxy的实例需要传入两个参数
1.target要被代理的对象,可以是一个object或者function
2.handlers对该代理对象的各种操作行为处理
lettarget={} lethandlers={}//donothing letproxy=newProxy(target,handlers) proxy.a=123 console.log(target.a)//123
在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝
(Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)
Traps(各种行为的代理)
就像上边的示例代码一样,如果没有定义对应的trap,则不会起任何作用,相当于直接操作了target。
当我们写了某个trap以后,在做对应的动作时,就会触发我们的回调函数,由我们来控制被代理对象的行为。
最常用的两个trap应该就是get和set了。
早年JavaScript有着在定义对象时针对某个属性进行设置getter、setter:
letobj={ _age:18, getage(){ return`I'm${this._age}yearsold` }, setage(val){ this._age=Number(val) } } console.log(obj.age)//I'm18yearsold obj.age=19 console.log(obj.age)//I'm19yearsold
就像这段代码描述的一样,我们设置了一个属性_age,然后又设置了一个getage和setage。
然后我们可以直接调用obj.age来获取一个返回值,也可以对其进行赋值。
这么做有几个缺点:
1.针对每一个要代理的属性都要编写对应的getter、setter。
2.必须还要存在一个存储真实值的key(如果我们直接在getter里边调用this.age则会出现堆栈溢出的情况,因为无论何时调用this.age进行取值都会触发getter)。
Proxy很好的解决了这两个问题:
lettarget={age:18,name:'NikoBellic'} lethandlers={ get(target,property){ return`${property}:${target[property]}` }, set(target,property,value){ target[property]=value } } letproxy=newProxy(target,handlers) proxy.age=19 console.log(target.age,proxy.age)//19,age:19 console.log(target.name,proxy.name)//NikoBellic,name:NikoBellic
我们通过创建get、set两个trap来统一管理所有的操作,可以看到,在修改proxy的同时,target的内容也被修改,而且我们对proxy的行为进行了一些特殊的处理。
而且我们无需额外的用一个key来存储真实的值,因为我们在trap内部操作的是target对象,而不是proxy对象。
拿Proxy来做些什么
因为在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。
在下边列出了几个简单的适用场景。
解决对象属性为undefined的问题
在一些层级比较深的对象属性获取中,如何处理undefined一直是一个痛苦的过程,如果我们用Proxy可以很好的兼容这种情况。
(()=>{ lettarget={} lethandlers={ get:(target,property)=>{ target[property]=(propertyintarget)?target[property]:{} if(typeoftarget[property]==='object'){ returnnewProxy(target[property],handlers) } returntarget[property] } } letproxy=newProxy(target,handlers) console.log('z'inproxy.x.y)//false(其实这一步已经针对`target`创建了一个x.y的属性) proxy.x.y.z='hello' console.log('z'inproxy.x.y)//true console.log(target.x.y.z)//hello })()
我们代理了get,并在里边进行逻辑处理,如果我们要进行get的值来自一个不存在的key,则我们会在target中创建对应个这个key,然后返回一个针对这个key的代理对象。
这样就能够保证我们的取值操作一定不会抛出cannotgetxxxfromundefined
但是这会有一个小缺点,就是如果你确实要判断这个key是否存在只能够通过in操作符来判断,而不能够直接通过get来判断。
普通函数与构造函数的兼容处理
如果我们提供了一个Class对象给其他人,或者说一个ES5版本的构造函数。
如果没有使用new关键字来调用的话,Class对象会直接抛出异常,而ES5中的构造函数this指向则会变为调用函数时的作用域。
我们可以使用apply这个trap来兼容这种情况:
classTest{ constructor(a,b){ console.log('constructor',a,b) } } //Test(1,2)//throwanerror letproxyClass=newProxy(Test,{ apply(target,thisArg,argumentsList){ //如果想要禁止使用非new的方式来调用函数,直接抛出异常即可 //thrownewError(`Function${target.name}cannotbeinvokedwithout'new'`) returnnew(target.bind(thisArg,...argumentsList))() } }) proxyClass(1,2)//constructor12
我们使用了apply来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个Class或构造函数,所以我们直接在apply中使用new关键字来调用被代理的函数。
以及如果我们想要对函数进行限制,禁止使用new关键字来调用,可以用另一个trap:construct
functionadd(a,b){ returna+b } letproxy=newProxy(add,{ construct(target,argumentsList,newTarget){ thrownewError(`Function${target.name}cannotbeinvokedwith'new'`) } }) proxy(1,2)//3 newproxy(1,2)//throwanerror
用Proxy来包装fetch
在前端发送请求,我们现在经常用到的应该就是fetch了,一个原生提供的API。
我们可以用Proxy来包装它,使其变得更易用。
lethandlers={ get(target,property){ if(!target.init){ //初始化对象 ['GET','POST'].forEach(method=>{ target[method]=(url,params={})=>{ returnfetch(url,{ headers:{ 'content-type':'application/json' }, mode:'cors', credentials:'same-origin', method, ...params }).then(response=>response.json()) } }) } returntarget[property] } } letAPI=newProxy({},handlers) awaitAPI.GET('XXX') awaitAPI.POST('XXX',{ body:JSON.stringify({name:1}) })
对GET、POST进行了一层封装,可以直接通过.GET这种方式来调用,并设置一些通用的参数。
实现一个简易的断言工具
写过测试的各位童鞋,应该都会知道断言这个东西
console.assert就是一个断言工具,接受两个参数,如果第一个为false,则会将第二个参数作为Errormessage抛出。
我们可以使用Proxy来做一个直接赋值就能实现断言的工具。
letassert=newProxy({},{ set(target,message,value){ if(!value)console.error(message) } }) assert['Isn\'ttrue']=false//Error:Isn'ttrue assert['Lessthan18']=18>=19//Error:Lessthan18
统计函数调用次数
在做服务端时,我们可以用Proxy代理一些函数,来统计一段时间内调用的次数。
在后期做性能分析时可能会能够用上:
functionorginFunction(){} letproxyFunction=newProxy(orginFunction,{ apply(target,thisArg.argumentsList){ log(XXX) returntarget.apply(thisArg,argumentsList) } })
全部的traps
这里列出了handlers所有可以定义的行为(traps):
具体的可以查看MDN-Proxy
里边同样有一些例子
traps | description |
---|---|
get | 获取某个key值 |
set | 设置某个key值 |
has | 使用in操作符判断某个key是否存在 |
apply | 函数调用,仅在代理对象为function时有效 |
ownKeys | 获取目标对象所有的key |
construct | 函数通过实例化调用,仅在代理对象为function时有效 |
isExtensible | 判断对象是否可扩展,Object.isExtensible的代理 |
deleteProperty | 删除一个property |
defineProperty | 定义一个新的property |
getPrototypeOf | 获取原型对象 |
setPrototypeOf | 设置原型对象 |
preventExtensions | 设置对象为不可扩展 |
getOwnPropertyDescriptor | 获取一个自有属性 (不会去原型链查找) 的属性描述 |
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。