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

看过一篇JS装饰器,这是一个会装扮的装饰器

时间:2023-03-20 00:33:05 科技观察

俗话说,人靠衣服,佛靠金装。大街上的姑娘们都喜欢把自己打扮得漂漂亮亮的,让你忍不住多看几眼,这就是装饰的作用。1.preambledecorator是最新的ECMA中的一个提案,是一种类相关的语法,用于注解或修改类和类方法。装饰器也在Python和Java等语言中大量使用。装饰器是实现AOP(面向切面)编程的重要方式。下面是一个使用装饰器的简单例子,这个@readonly可以设置count属性为只读。可以看出,装饰器大大提高了代码的简洁性和可读性。classPerson{@readonlycount=0;}因为浏览器还不支持装饰器,为了让大家能正常看到效果,这里我使用Parcel进行了简单的配置,大家可以去clone这个仓库,运行所有的东西本文涉及的例子,仓库地址:[学习es6][https://github.com/yingguangyao/ES6]本文涉及[Object.defineProperty][https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty]、高阶函数等知识,如果之前没有了解过相关概念,建议了解后再阅读本文。2.装饰者模式在开始讲解装饰者之前,我们先从经典的装饰者模式说起。装饰器模式是一种结构设计模式,它允许在不改变现有对象结构的情况下向现有对象添加新功能,作为现有类的包装器。一般来说,在代码设计中,我们应该遵循“多组合,少继承”的原则。通过装饰器模式动态地为一个对象添加一些额外的职责。装饰器模式在添加功能方面比子类化更灵活。2.1英雄联盟的一个例子。下班回来,和朋友们玩得很开心。我在用亚索“迎风而行”的时候突然想到,如果让我设计一个亚索英雄,我应该怎么实现呢?我想了想,我肯定先设计一个英雄类。classHero{attack(){}}然后实现一个亚索类继承英雄类。classYasuoextendsHero{attack(){console.log("SteelFlash");}}当我还在思考这个问题的时候,我的队友已经打到了大龙,我的亚索身上出现了小龙buff的印记。突然想到,怎么给英雄加龙buff呢?加个大龙buff的属性不好吗?当然不是,要知道,英雄联盟里面的大龙buff是会增加收入的。嗯,聪明的我已经想出办法了,再继承下去不是很好吗?classBaronYasuoextendsYasuo{}很棒,但如果Yasuo有其他增益怎么办?毕竟LOL里面有红buff、蓝buff、龙buff等等,有多少分类就不用加多少了?你可以换个角度思考这个问题,如果你把buff想象成我们的衣服。在不同的季节,我们会换上不同的衣服,到了冬天,我们甚至会叠穿多件衣服。当buff消失的时候,就相当于脱掉了衣服。如下图所示:衣服是给人的装饰,buff对亚索来说只是加成。那么,你有什么想法吗?没错,你可以创建一个Buff类并将其传递给Hero类,从而得到一个新的增强型Hero类。classRedBuffextendsBuff{constructor(hero){this.hero=hero;}//红色buff造成额外伤害extraDamage(){}attack(){returnthis.hero.attack()+this.extraDamage();}}classBlueBuffextendsBuff{constructor(hero){this.hero=hero;}//技能CD(负10%)CDR(){returnthis.hero.CDR()*0.9;}}classBaronBuffextendsBuff{constructor(hero){this.hero=hero;}//回城速度减半backSpeed(){returnthis.hero.backSpeed*0.5;}}定义完所有buff类后,可以直接作用于英雄。是不是看起来清爽了很多?这个符号看起来很像函数组合。constyasuo=newYasuo();constredYasuo=newRedBuff(yasuo);//红buff亚索constblueYasuo=newBlueBuff(yasuo);//蓝buff亚索constredBlueYasuo=newBlueBuff(redYasuo);//红蓝buff亚索3.ES7装饰Decorator(decorator)是ES7中的提案,目前处于stage-2阶段,提案地址:[JavaScriptDecorators][https://github.com/tc39/proposal-decorators]。装饰器与前面提到的函数组合(compose)和高阶函数非常相似。装饰器使用@作为标识符,放在要装饰的代码之前。在其他语言中,已经有比较成熟的装饰器方案。3.1Python中的装饰器让我们看一个Python中装饰器的例子:defauth(func):definner(request,*args,**kwargs):v=request.COOKIES.get('user')ifnotv:returnredirect('/login')returnfunc(request,*args,**kwargs)returninner@authdefindex(request):v=request.COOKIES.get("user")returnrender(request,"index.html",{"current_user":v})这个auth装饰器检查cookie以查看用户是否登录。auth函数是一个高阶函数,它将func函数作为参数并返回一个新的内部函数。innerfunction中查看cookie,判断是跳回登录页面还是继续执行func函数。这个auth装饰器可以用在所有需要权限验证的函数上,简洁无侵入。3.2JavaScript装饰器JavaScript中的装饰器类似于Python的装饰器,依赖于Object.defineProperty,一般用来装饰类、类属性和类方法。使用装饰器可以在不直接修改代码的情况下实现某些功能,从而实现真正的面向切面编程。这在一定程度上类似于Proxy,但是使用起来比Proxy更简洁。注意:装饰器目前处于第2阶段,这意味着语法将来可能会发生变化。已经有一些计划在函数、对象等中使用装饰器,请参见:[未来内置装饰器][https://github.com/tc39/proposal-decorators/blob/master/NEXTBUILTINS.md#applying-built-in-decorators-to-other-syntactic-forms]3.3类装饰器装饰类时,装饰器方法一般接收一个目标类作为参数。下面是给目标类添加静态属性test的例子:constdecoratorClass=(targetClass)=>{targetClass.test='123'}@decoratorClassclassTest{}Test.test;//'123'不仅可以修饰类本身,也可以通过修改原型,给实例添加新的属性。下面是一个给目标类添加speak方法的例子:}}@withSpeakclassStudent{constructor(language){this.language=language;}}conststudent1=newStudent('Chinese');conststudent2=newStudent('English');student1.speak();//IcanspeakChinesestudent2.speak();//IcanspeakChinese使用高阶函数的属性也可以给装饰器传递参数,通过参数来决定对类做什么。constwithLanguage=(language)=>(targetClass)=>{targetClass.prototype.language=language;}@withLanguage('Chinese')classStudent{}conststudent=newStudent();student.language;//'Chinese'如果你经常在写react-redux代码的时候,也会遇到将store中的数据映射到组件中的需求。connect是一个高层组件,它接收两个函数mapStateToProps和mapDispatchToProps以及一个组件App,最后返回一个增强版的组件。classAppextendsReact.Component{}connect(mapStateToProps,mapDispatchToProps)(App)有了装饰器,connect的写法可以变得更加优雅。@connect(mapStateToProps,mapDispatchToProps)classAppextendsReact.Component{}3.4类属性装饰器类属性装饰器可以用在类属性、方法、get/set函数中,一般接收三个参数:target:要装饰的类名:nameoftheclassmemberdescriptor:属性描述符,对象会把这个参数传递给Object.defineProperty使用类属性装饰器可以做很多有趣的事情,比如开头给出的readonly例子:functionreadonly(target,name,descriptor){descriptor.writable=false;returndescriptor;}classPerson{@readonlyname='person'}constperson=newPerson();person.name='tom';也可以用来统计一个函数的执行时间,方便后面做一些性能优化。函数时间(目标、名称、描述符){constfunc=descriptor.value;if(typeoffunc==='function'){descriptor.value=function(...args){console.time();constresults=func.apply(这个,args);console.timeEnd();returnresults;}}}classPerson{@timesay(){console.log('hello')}}constperson=newPerson();person.say();well-knownstate在react管理库mobx中,也通过装饰器将类属性设置为可观察属性,实现响应式编程。import{observable,action,autorun}from'mobx'classStore{@observablecount=1;@actionchangeCount(count){this.count=count;}}conststore=newStore();autorun(()=>{console.log('countis',store.count);})store.changeCount(10);//修改count的值会导致autorun中的函数自动执行。3.5装饰器组合如果要使用多个装饰器怎么办?装饰器可以根据被装饰类/属性的远近,依次叠加执行。classPerson{@time@logsay(){}}另外,在装饰器提案中,还有一个装饰器组合多个装饰器的例子。还没有看到它被使用。通过使用装饰器声明一个组合装饰器xyz,组合了多个装饰器。decorator@xyz(arg,arg2{@foo@bar(arg)@baz(arg2)}@xyz(1,2)classC{}和下面的一样。@foo@bar(1)@baz(2)classC{}4.装饰器可以做什么有趣的事情?4.1多重继承在讲解JavaScript多重继承之前,我们使用了mixin方法,装饰器的组合甚至可以进一步简化mixin的使用。mixin方法会收到一个列表父类并用它来装饰目标类,我们理想的用法应该是这样的:@mixin(Parent1,Parent2,Parent3)classChild{}和前面多重继承的实现一样,只是复制原型属性和实例属性可以实现父类的实现,这里新建一个Mixin类,把mixins和targetClass上面的所有属性都复制过来。constmixin=(...mixins)=>(targetClass)=>{mixins=[targetClass,...mixins];functioncopyProperties(target,source){for(letkeyofReflect.ownKeys(source)){if(key!=='constructor'&&key!=='prototype'&&key!=='name'){letdesc=Object.getOwnPropertyDescriptor(source,key);Object.defineProperty(target,key,desc);}}}classMixin{constructor(...args){for(letmixinofmixins){copyProperties(this,newmixin(...args));//复制实例属性}}}for(letmixinofmixins){copyProperties(Mixin,mixin);//复制静态属性copyProperties(Mixin.prototype,mixin.prototype);//复制原型属性}returnMixin;}exportdefaultmixin下面测试一下这个mixin方法是否正常.classParent1{p1(){console.log('thisisparent1')}}classParent2{p2(){console.log('thisisparent2')}}classParent3{p3(){console.log('thisisparent3')}}@mixin(Parent1,Parent2,Parent3)classChild{c1=()=>{console.log('thisischild')}}constchild=newChild();console.log(child);最终在浏览器中打印出来的子对象是这样的,证明这个mixin可以正常工作。注意:这里的Child类就是之前的Mixin类。image.png-69.4kB也许你会问,为什么要多创建一个Mixin类?为什么我们不能直接修改targetClass的构造函数呢?不是说Proxy可以拦截构造函数吗?恭喜你想到了Proxy的一个使用场景。是的,这里使用Proxy确实更优雅。constmixin=(...mixins)=>(targetClass)=>{functioncopyProperties(target,source){for(letkeyofReflect.ownKeys(source)){if(key!=='constructor'&&key!=='prototype'&&key!=='name'){letdesc=Object.getOwnPropertyDescriptor(source,key);Object.defineProperty(target,key,desc);}}}for(letmixinofmixins){copyProperties(targetClass,mixin);//复制静态属性copyProperties(targetClass.prototype,mixin.prototype);//复制原型属性}//拦截构造方法复制实例属性returnnewProxy(targetClass,{construct(target,args){constobj=newtarget(...args);for(letmixinofmixins){copyProperties(obj,newmixin());//复制实例属性}returnobj;}});}4.2防抖和节流以前我们经常在频繁触发的场景中使用节流功能来优化性能。我们以React组件绑定滚动事件为例:(){window.removeEveneListener('scroll',this.handleScroll);}scroll(){}}在组件中绑定事件时,应该在组件销毁时解除绑定。由于节流函数返回的是一个新的匿名函数,为了后面能够有效解绑,这个匿名函数不得不保存起来,以备后用。但是有了装饰器之后,我们就不用在每个绑定事件的地方都手动设置throttle方法了,只需要在scroll函数中添加一个throttle装饰器即可。constthrottle=(time)=>{letprev=newDate();return(target,name,descriptor)=>{constfunc=descriptor.value;if(typeoffunc==='function'){descriptor.value=function(...args){constnow=newDate();if(now-prev>wait){fn.apply(this,args);prev=newDate();}}}}}使用起来比以前简洁多了。classAppextendsReact.Component{componentDidMount(){window.addEveneListener('scroll',this.scroll);}componentWillUnmount(){window.removeEveneListener('scroll',this.scroll);}@throttle(50)scroll(){}debounce函数装饰器和throttling函数类似,这里就不多说了。constdebounce=(time)=>{lettimer;return(target,name,descriptor)=>{constfunc=descriptor.value;if(typeoffunc==='function'){descriptor.value=function(...args){if(timer)clearTimeout(timer)timer=setTimeout(()=>{fn.apply(this,args)},wait)}}}}对节流防抖功能感兴趣的可以看这篇文章:【函数节流与函数防抖】[https://juejin.im/entry/58c0379e44d9040068dc952f]4.3数据格式校验使用类属性装饰器对类属性进行类型校验。constvalidate=(type)=>(target,name)=>{if(typeoftarget[name]!==type){thrownewError(`attribute${name}mustbe${type}type`)}}classForm{@validate('string')name=111//Error:attributenamemustbe${type}type}如果觉得手工逐一验证属性太麻烦,也可以通过编写验证规则来验证整个类。construles={name:'string',password:'string',age:'number'}constvalidator=rules=>targetClass=>{returnnewProxy(targetClass,{construct(target,args){constobj=newtarget(...args);for(let[name,type]ofObject.entries(rules)){if(typeofobj[name]!==type){thrownewError(`${name}必须是${type}`)}}returnobj;}})}@validator(rules)classPerson{name='tom'password='123'age='21'}constperson=newPerson();4.4core-decorators.jscore-decorators是一个封装常用装饰器的JS库。它总结了以下装饰器(仅列出了几个)。autobind:自动绑定this,告别箭头函数绑定readonly:设置类属性为只读override:检查子类的方法是否正确覆盖了父类的同名方法一个类的方法变成可枚举的nonenumerable:makeaclasspropertynonenumerabletime:printfunctionexecutiontime-consumedmixin:mixmultipleobjectsintotheclass(和我们上面的mixin不一样)

猜你喜欢