Javascript装饰器原理分析
这篇文章将为大家详细讲解有关Javascript装饰器原理分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。
一个以@开头的描述性词语。英语的decorator动词是decorate,装饰的意思。其中词根dek(dec发音)原始印欧语系中意思是“接受”。即,原来的某个事物接受一些新东西(而变得更好)。
从另外一个角度描述,装饰器主要是在被装饰对象的外部起作用,而非入侵其内部发生什么改变。装饰器模式同时也是一种开发模式,其地位虽然弱于MVC、IoC等,但不失为一种优秀的模式。
JavaScript的装饰器可能是借鉴自Python也或许是Java。较为明显的不同的是大部分语言的装饰器必须是一行行分开,而js的装饰器可以在一行中。
装饰器存在的意义举个例子:我拿着员工卡进入公司总部大楼。因为每个员工所属的部门、级别不同,并不能进入大楼的任何房间。每个房间都有一扇门;那么,公司需要安排每个办公室里至少一个人关于验证来访者的工作:
先登记来访者
验证是否有权限进入,如果没有则要求其离开
记录其离开时间
还有一个选择方式,就是安装电子门锁,门锁只是将员工卡的信息传输给机房,由特定的程序验证。
前者暂且称之为笨模式,代码如下:
functionA101(who){record(who,newDate(),'enter');if(!permission(who)){record(who,newDate(),'nopermission')returnvoid;}//继续执行doSomeWork();record(who,newDate(),'leave')}functionA102(who){record(who,newDate(),'enter');if(!permission(who)){record(who,newDate(),'nopermission')returnvoid;}//继续执行doSomeWork();record(who,newDate(),'leave')}//...
有经验的大家肯定第一时间想到了,把那些重复语句封装为一个方法,并统一调用。是的,这样可以解决大部分问题,但是还不够“优雅”。同时还有另外一个问题,如果“房间”特别多,又或者只有大楼奇数号房间要验证偶数不验证,那岂不是很“变态”?如果使用装饰器模式来做,代码会如下面这样的:
@verify(who)classBuilding{@verify(who)A101(){/*...*/}@verify(who)A102(){/*...*/}//...}
verify是验证的装饰器,而其本质就是一组函数。
JavaScript装饰器正如先前的那个例子,装饰器其实本身就是一个函数,它在执行被装饰的对象之前先被执行。
在JavaScript中,装饰器的类型有:
类
存取方法(属性的get和set)
字段
方法
参数
由于目前装饰器概念还处于提案阶段,不是一个正式可用的js功能,所以想要使用这个功能,不得不借助翻译器工具,例如Babel工具或者TypeScript编译JS代码转后才能被执行。我们需要先搭建运行环境,配置一些参数。(以下过程,假设已经正确安装了NodeJS开发环境以及包管理工具)
cdproject&&npminitnpmi-D@babel/cli@babel/core@babel/plugin-proposal-class-properties@babel/plugin-proposal-decorators@babel/preset-envbabel-plugin-parameter-decorator
创建一个.babelrc配置文件,如下:
{"presets":["@babel/preset-env"],"plugins":[["@babel/plugin-proposal-decorators",{"legacy":true}],["@babel/plugin-proposal-class-properties",{"loose":true}],"babel-plugin-parameter-decorator"]}
利用下面的转换命令,我们可以得到ES5的转换程序:
npx babel source.js --out-file target.js类装饰器
创建一个使用装饰器的JS程序decorate-class.js
@classDecoratorclassBuilding{constructor(){this.name="company";}}constbuilding=newBuilding();functionclassDecorator(target){console.log("target",target);}
以上是最最简单的装饰器程序,我们利用babel将其“翻译”为ES5的程序,然后再美化一下后得到如下程序。
"usestrict";var_class;function_classCallCheck(instance,Constructor){if(!(instanceinstanceofConstructor)){thrownewTypeError("Cannotcallaclassasafunction");}}varBuilding=classDecorator((_class=functionBuilding(){_classCallCheck(this,Building);this.name="company";}))||_class;varbuilding=newBuilding();functionclassDecorator(target){console.log("target",target);}
第12行就是在类生成过程中,调用函数形态的装饰器,并将构造函数(类本身)送入其中。同样揭示了装饰器的第一个参数是类的构造函数的由来。
方法 (method)装饰器稍微修改一下代码,依旧是尽量保持最简单:
classBuilding{constructor(){this.name="company";}@methodDecoratoropenDoor(){console.log("Thedoorbeingopen");}}constbuilding=newBuilding();functionmethodDecorator(target,property,descriptor){console.log("target",target);if(property){console.log("property",property);}if(descriptor){console.log("descriptor",descriptor);}console.log("=====endofdecorator=========");}
然后转换代码,可以发现,这次代码量突然增大了很多。排除掉_classCallCheck、_defineProperties和_createClass三个函数,关注_applyDecoratedDescriptor函数:
function_applyDecoratedDescriptor(target,property,decorators,descriptor,context){vardesc={};Object.keys(descriptor).forEach(function(key){desc[key]=descriptor[key];});desc.enumerable=!!desc.enumerable;desc.configurable=!!desc.configurable;if("value"indesc||desc.initializer){desc.writable=true;}desc=decorators.slice().reverse().reduce(function(desc,decorator){returndecorator(target,property,desc)||desc;},desc);if(context&&desc.initializer!==void0){desc.value=desc.initializer?desc.initializer.call(context):void0;desc.initializer=undefined;}if(desc.initializer===void0){Object.defineProperty(target,property,desc);desc=null;}returndesc;}
它在生成构造函数之后,执行了这个函数,特别注意,这个装饰器函数是以数组形式的参数传递的。然后到上述代码的17~22行,将装饰器逐个应用,其中对装饰器的调用就在第21行。
它发送了3个参数,target指类本身。property指方法名(或者属性名),desc是可能被先前装饰器被处理过的descriptor,如果是第一次循环或只有一个装饰器,那么就是方法或属性本身的descriptor。
存取器(accessor)装饰JS关于类的定义中,支持get和set关键字针对设置某个字段的读写操作逻辑,装饰器也同样支持这类方法的操作。
classBuilding{constructor(){this.name="company";}@propertyDecoratorgetroomNumber(){returnthis._roomNumber;}_roomNumber="";openDoor(){console.log("Thedoorbeingopen");}}
有心的读者可能已经发现了,存取器装饰的代码与上面的方法装饰代码非常接近。关于属性 get和set方法,其本身也是一种方法的特殊形态。所以他们之间的代码就非常接近了。
属性装饰器继续修改源代码:
classBuilding{constructor(){this.name="company";}@propertyDecoratorroomNumber="";}constbuilding=newBuilding();functionpropertyDecorator(target,property,descriptor){console.log("target",target);if(property){console.log("property",property);}if(descriptor){console.log("descriptor",descriptor);}console.log("=====endofdecorator=========");}
转换后的代码,还是与上述属性、存取器的代码非常接近。但除了_applyDecoratedDescriptor外,还多了一个_initializerDefineProperty函数。这个函数在生成构造函数时,将声明的各种字段绑定给对象。
参数装饰器参数装饰器的使用位置较之前集中装饰器略有不同,它被使用在行内。
classBuilding{constructor(){this.name="company";}openDoor(@parameterDecoratornum,@parameterDecoratorzoz){console.log(`${num}doorbeingopen`);}}constbuilding=newBuilding();functionparameterDecorator(target,property,key){console.log("target",target);if(property){console.log("property",property);}if(key){console.log("key",key);}console.log("=====endofdecorator=========");}
转换后的代码区别就比较明显了,babel并没有对其生成一个特定的函数对其进行特有的操作,而只在创建完类(构造函数)以及相关属性、方法后直接调用了开发者自己编写的装饰器函数:
varBuilding=/*#__PURE__*/function(){functionBuilding(){_classCallCheck(this,Building);this.name="company";}_createClass(Building,[{key:"openDoor",value:functionopenDoor(num,zoz){console.log("".concat(num,"doorbeingopen"));}}]);parameterDecorator(Building.prototype,"openDoor",1);parameterDecorator(Building.prototype,"openDoor",0);returnBuilding;}();装饰器应用使用参数——闭包
以上所有的案例,装饰器本身均没有使用任何参数。然实际应用中,经常会需要有特定的参数需求。我们再回到一开头的例子中verify(who),其中需要传入一个身份变量。哪又怎么做?我们少许改变一下类装饰器的代码:
constwho="Django";@classDecorator(who)classBuilding{constructor(){this.name="company";}}
转换后得到
//...varwho="Django";varBuilding=((_dec=classDecorator(who)),_dec((_class=functionBuilding(){_classCallCheck(this,Building);this.name="company";}))||_class);//...
请注意第4第5行,它先执行了装饰器,然后再用返回值将类(构造函数)送入。相对应的,我们就应该将构造函数写成下面这样:
functionclassDecorator(people){console.log(`hi~${people}`);returnfunction(target){console.log("target",target);};}
同样的,方法、存取器、属性和参数装饰器均是如此。
装饰器包裹方法到此,我们已经可以将装饰器参数与目标对象结合起来,进行一些逻辑类的操作。那么再回到文章的开头的例子中:需求中要先验证来访者权限,然后记录,最后在来访者离开时再做一次记录。此时需要监管对象方法被调用的整个过程。
请大家留意那个方法装饰器的descriptor,我们可以利用这个对象来“重写”这个方法。
classBuilding{constructor(){this.name="company";}@methodDecorator("Gate")openDoor(firstName,lastName){return`Thedoorwillbeopen,when${firstName}${lastName}iswalkingintothe${this.name}.`;}}letbuilding=newBuilding();console.log(building.openDoor("django","xiang"));functionmethodDecorator(door){returnfunction(target,property,descriptor){letfn=descriptor.value;descriptor.value=function(...args){let[firstName,lastName]=args;console.log(`log:${firstName},whoarecomming.`);//verify(firstName,lastName)letresult=Reflect.apply(fn,this,[firstName,lastName]);console.log(`log:${result}`);console.log(`log:${firstName},whoareleaving.`);returnresult;};returndescriptor;};}
代码第17行,将原方法暂存;18行定义一个新的方法,20~25行,记录、验证和记录离开的动作。
log:Django,whoarecomming.log:Thedoorwillbeopen,whenDjangoXiangiswalkingintothecompany.log:Django,whoareleaving.Thedoorwillbeopen,whenDjangoXiangiswalkingintothecompany装饰顺序
通过阅读转换后的代码,我们知道装饰器工作的时刻是在类被实例化之前,在生成之中完成装饰函数的动作。那么,如果不同类型的多个装饰器同时作用,其过程是怎样的?我们将先前的案例全部整合到一起看看:
constwho="Django";@classDecorator(who)classBuilding{constructor(){this.name="company";}@propertyDecoratorroomNumber="";@methodDecoratoropenDoor(@parameterDecoratornum){console.log(`${num}doorbeingopen`);}@accessorDecoratorgetroomNumber(){returnthis._roomNumber;}}constbuilding=newBuilding();functionclassDecorator(people){console.log(`classdecorator`);returnfunction(target){console.log("target",target);};}functionmethodDecorator(target,property,descriptor){console.log("methoddecorator");}functionaccessorDecorator(target,property,descriptor){console.log("accessordecorator");}functionpropertyDecorator(target,property,descriptor){console.log("propertydecoator");}functionparameterDecorator(target,property,key){console.log("parameterdecorator");}
class decorator
parameter decorator
property decoator
method decorator
accessor decorator
还可以通过阅读转换后的源代码得到执行顺序:
类装饰器(在最外层)
参数装饰器(在生成构造函数最里层)
按照出现的先后顺序的:属性、方法和存取器
关于“Javascript装饰器原理分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。