当前位置: 首页 > Web前端 > HTML5

从ES6重新认识JavaScript设计模式:装饰者模式

时间:2023-04-06 00:25:08 HTML5

1什么是装饰者模式?在不改变现有对象结构的情况下向现有对象添加新功能的设计模式称为装饰器模式(DecoratorPattern),它充当现有类的包装器。装饰器可以理解为游戏角色购买的装备。比如LOL中的英雄,刚开始游戏的时候只有基础攻击力和法术强度。但购买装备后,在触发攻击和技能时,可以享受装备带来的输出加成。我们可以理解为购买的装备上装饰着英雄的攻击方式和技能。这是淘宝前端团队的一篇博文。以钢铁侠为例来讲解装饰者模式,很有意思。2ESnext中的Decorator模式ESnext中有一个Decorator提案,它使用@开头的函数来装饰ES6中的类及其属性和方法。Decorator的详细语法可以参考阮一峰的《ECMASciprt入门 —— Decorator》。目前Decorator的语法只是一个提议。如果现在要使用装饰器模式,需要安装配合babel+webpack,结合plugins。npm安装依赖npminstallbabel-corebabel-loaderbabel-plugin-transform-decoratorsbabel-plugin-transform-decorators-legacybabel-preset-envconfiguration.babelrcfile{"presets":["env"],"plugins":["transform-decorators-legacy"]}在webpack.config.js中添加babel-loader模块:{rules:[{test:/\.js$/,exclude:/node_modules/,loader:"babel-loader"}],}如果你使用的IDE是VisualStudioCode,你可能还需要在项目根目录下添加如下tsconfig.json文件来组织一次ts校验报错。{"compilerOptions":{"experimentalDecorators":true,"allowJs":true,"lib":["es6"],}}下面我将实现3个装饰器,分别是@autobind、@debounce、@deprecate。2.1@autobind实现this指向原始对象在JavaScript中,this的指向一直是一个常见的话题。新手在使用Vue或者React等框架的过程中,可能会不小心丢失this的指向,导致Invocation错误。例如下面这段代码:classPerson{getPerson(){returnthis;}}letperson=newPerson();let{getPerson}=person;console.log(getPerson()===person);//false上面的代码中getPerson方法中的this默认指向Person类的实例,但是如果通过解构赋值提取Person,那么此时this指向undefined。所以最后的打印结果是假的。此时,我们可以实现一个autobind函数来修饰getPerson方法,使this始终指向Person实例。functionautobind(target,key,descriptor){varfn=descriptor.value;varconfigurable=descriptor.configurable;varenumerable=descriptor.enumerable;//返回描述符return{configurable:configurable,enumerable:enumerable,get:functionget(){//将方法绑定到thisvarboundFn=fn.bind(this);//使用Object.defineProperty重新定义方法Object.defineProperty(this,key,{configurable:true,writable:true,enumerable:false,value:boundFn})returnboundFn;}}}我们通过bind实现了this的绑定,在get中使用Object.defineProperty重写了方法,将value定义为bind绑定的函数boundFn,从而实现this始终指向实例。接下来我们装饰并调用getPerson方法。类Person{@autobindgetPerson(){返回这个;}}letperson=newPerson();let{getPerson}=person;console.log(getPerson()===person);//true2.2@debounce实现功能防抖功能防抖(debounce)在前端项目中有很多应用,比如在resize或者scroll等事件中操作DOM,或者实现用户输入的实时ajax搜索等会频繁触发,前者会影响浏览器。性能有直观的影响,后者会给服务器带来很大的压力。我们期望这样的高频连续触发事件在触发结束后响应。这就是功能防抖的应用。classEditor{constructor(){this.content='';}updateContent(content){console.log(content);this.content=内容;//后面还有一些比较耗性能的操作}}consteditor1=newEditor();editor1.updateContent(1);setTimeout(()=>{editor1.updateContent(2);},400);consteditor2=newEditor();editor2.updateContent(3);setTimeout(()=>{editor2.updateContent(4);},600);//Printresult:1324在上面的代码中,我们定义了类Editor,其中updateContent方法会在用户进入时执行,可能会有一些比较耗性能的DOM操作,这里我们打印方法内部传入的参数来验证调用过程。可以看出4次调用的结果分别是1324。下面我们实现一个传入数字超时参数的去抖函数。functiondebounce(timeout){constinstanceMap=newMap();//创建一个Map数据结构,以实例化对象为keyinstanceMap.get(this));//设置延迟器instanceMap.set(this,setTimeout(()=>{//调用这个方法descriptor.value.apply(this,arguments);//设置延迟器为空instanceMap.set(this,null);},timeout));}})}}在上面的方法中,我们使用了ES6Structure提供的Map数据来实现实例化对象和延迟器的映射。函数内部,先清除delayer,然后设置延迟执行函数,这是实现debounce的通用方法,我们来测试一下debounce装饰器。classEditor{constructor(){this.content='';}@debounce(500)updateContent(content){console.log(content);this.content=内容;}}consteditor1=newEditor();editor1。updateContent(1);setTimeout(()=>{editor1.updateContent(2);},400);consteditor2=newEditor();editor2.updateContent(3);setTimeout(()=>{editor2.updateContent(4);},600);//打印结果:324上面调用了4次updateContent方法,打印结果为324.1没有打印是因为在400ms内重复调用了,符合我们预期的参数500。2.3@deprecate实现警告提示在使用第三方库的过程中,我们会时不时的在控制台遇到一些警告。这些警告用于提醒开发者调用的方法将在下一个版本中弃用。这样一行打印信息可能是我们平时的做法是在方法内部加一行代码,其实对源码阅读不友好,也不符合单一职责原则。如果在需要抛出警告的方法前加上@deprecate装饰器来实现警告,会友好很多。接下来,让我们实现一个@deprecate装饰器。其实这种装饰器还可以扩展为打印日志装饰器@log、报表信息装饰器@fetchInfo等。functiondeprecate(deprecatedObj){returnfunction(target,key,descriptor){constdeprecatedInfo=deprecatedObj.info;constdeprecatedUrl=deprecatedObj.url;//警告消息consttxt=`DEPRECATION${target.constructor.name}#${key}:${deprecatedInfo}.${已弃用的网址?'有关更多详细信息,请参见'+deprecatedUrl+':''}`;returnObject.assign({},descriptor,{value:functionvalue(){//打印警告消息console.warn(txt);descriptor.value.apply(this,arguments);}})}}上面的弃用函数接受一个对象参数,它有两个键值,分别是info和url,其中info填写Warningmessage,url是可选的详细网页地址。让我们将这个装饰器添加到名为MyLib的库的deprecatedMethod方法中!classMyLib{@deprecate({info:'这些方法将在下一个版本中弃用',url:'http://www.baidu.com'})deprecatedMethod(txt){console.log(txt)}}constlib=newMyLib();lib.deprecatedMethod('调用了一个将在下一版本中删除的方法');//DEPRECATIONMyLib#deprecatedMethod:该方法将在下一版本中弃用。详见http://www.baidu.com//调用了下个版本将去除的方法3总结通过ESnext中的装饰器实现装饰器模式,不仅具有扩展类功能的作用,而且在阅读源码的过程中还起到提示作用。上面的例子只是结合了装饰器的新语法和装饰器模式的简单封装,请不要在生产环境中使用。如果你已经意识到装饰器模式的好处,并想在你的项目中广泛使用它,不妨看看core-decorators库,它封装了很多常用的装饰器。参考IMWeb的前端博客:浅谈JS装饰器模式淘宝前端团队:ES7Decorator装饰器模式阮一峰:ECMAScript6入门