向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式(Decorator Pattern),它是做为现有的类的一个包装(Wrapper)。
能够将装饰器理解为游戏人物购买的装备,例如LOL中的英雄刚开始游戏时只有基础的攻击力和法强。可是在购买的装备后,在触发攻击和技能时,可以享受到装备带来的输出加成。咱们能够理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。前端
这里推荐一篇淘宝前端团队的博文,颇有趣的以钢铁侠的例子来说解了装饰者模式。node
ESnext中有一个Decorator
的提案,使用一个以 @
开头的函数对ES6中的class
及其属性、方法进行修饰。Decorator
的详细语法请参考阮一峰的《ECMASciprt入门 —— Decorator》。webpack
目前Decorator
的语法还只是一个提案,若是指望如今使用装饰器模式,须要安装配合babel
+ webpack
并结合插件实现。git
npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
.babelrc
文件es6
{
"presets": ["env"],
"plugins": ["transform-decorators-legacy"]
}<ul><li>在<code>webpack.config.js</code>中添加<code>babel-loader</code> </li></ul>
module: {
rules: [
{ test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }
],
}github<p>若是你使用的IDE为Visual Studio Code,可能还须要在项目根目录下添加如下<code>tsconfig.json</code>文件来组织一个ts检查的报错。</p> <pre><code class="JSON">{ "compilerOptions": { "experimentalDecorators": true, "allowJs": true, "lib": [ "es6" ], } }
下面我将实现3个装饰器,分别为
@autobind
、@debounce
、@deprecate
。web2.1
@autobind
实现this
指向原对象在JavaScript中,
this
的指向问题一直是一个老生常谈的话题,在Vue或React这类框架的使用过程当中,新手颇有可能一不当心就丢失了this
的指向致使方法调用错误。例以下面一段代码:ajaxclass Person { getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // false
上面的代码中,
getPerson
方法中的this
默认指向Person
类的实例,可是若是将Person
经过解构赋值的方式提取出来,那么此时的this
指向为undefined
。因此最终的打印结果为false
。npm此时咱们能够实现一个
autobind
的函数,用来装饰getPerson
这个方法,实现this
永远指向Person
的实例。jsonfunction autobind(target, key, descriptor) { var fn = descriptor.value; var configurable = descriptor.configurable; var enumerable = descriptor.enumerable; // 返回descriptor return { configurable: configurable, enumerable: enumerable, get: function get() { // 将该方法绑定this var boundFn = fn.bind(this); // 使用Object.defineProperty从新定义该方法 Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }) return boundFn; } } }
咱们经过
bind
实现了this
的绑定,并在get
中利用Object.defineProperty
重写了该方法,将value
定义为经过bind
绑定后的函数boundFn
,以此实现了this
永远指向实例。下面咱们为getPerson
方法加上装饰并调用。class Person { @autobind getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // true
2.2
@debounce
实现函数防抖函数防抖(debounce)在前端项目中有着不少的应用,例如在
resize
或scroll
等事件中操做DOM,或对用户输入实现实时ajax搜索等会被高频的触发,前者会对浏览器性能产生直观的影响,后者会对服务器产生较大的压力,咱们指望这类高频连续触发的事件在触发结束后再作出响应,这就是函数防抖的应用。class Editor { constructor() { this.content = ''; } updateContent(content) { console.log(content); this.content = content; // 后面有一些消耗性能的操做 } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); // 打印结果: 1 3 2 4
上面的代码中咱们定义了
Editor
这个类,其中updateContent
方法会在用户输入时执行并可能有一些消耗性能的DOM操做,这里咱们在该方法内部打印了传入的参数以验证调用过程。能够看到4次的调用结果分别为1 3 2 4
。下面咱们实现一个
debounce
函数,该方法传入一个数字类型的timeout
参数。function debounce(timeout) { const instanceMap = new Map(); // 建立一个Map的数据结构,将实例化对象做为key return function (target, key, descriptor) { return Object.assign({}, descriptor, { value: function value() { // 清除延时器 clearTimeout(instanceMap.get(this)); // 设置延时器 instanceMap.set(this, setTimeout(() => { // 调用该方法 descriptor.value.apply(this, arguments); // 将延时器设置为 null instanceMap.set(this, null); }, timeout)); } }) } }
上面的方法中,咱们采用了ES6提供的
Map
数据结构去实现实例化对象和延时器的映射。在函数的内部,首先清除延时器,接着设置延时执行函数,这是实现debounce
的通用方法,下面咱们来测试一下debounce
装饰器。class Editor { constructor() { this.content = ''; } @debounce(500) updateContent(content) { console.log(content); this.content = content; } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); //打印结果: 3 2 4
上面调用了4次
updateContent
方法,打印结果为3 2 4
。1
因为在400ms
内被重复调用而没有被打印,这符合咱们的参数为500
的预期。2.3
@deprecate
实现警告提示在使用第三方库的过程当中,咱们会时不时的在控制台碰见一些警告,这些警告用来提醒开发者所调用的方法会在下个版本中被弃用。这样的一行打印信息也许咱们的常规作法是在方法内部添加一行代码便可,这样其实在源码阅读上并不友好,也不符合单一职责原则。若是在须要抛出警告的方法前面加一个
@deprecate
的装饰器来实现警告,会友好得多。下面咱们来实现一个
@deprecate
的装饰器,其实这类的装饰器也能够扩展成为打印日志装饰器@log
,上报信息装饰器@fetchInfo
等。function deprecate(deprecatedObj) { return function(target, key, descriptor) { const deprecatedInfo = deprecatedObj.info; const deprecatedUrl = deprecatedObj.url; // 警告信息 const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`; return Object.assign({}, descriptor, { value: function value() { // 打印警告信息 console.warn(txt); descriptor.value.apply(this, arguments); } }) } }
上面的
deprecate
函数接受一个对象参数,该参数分别有info
和url
两个键值,其中info
填入警告信息,url
为选填的详情网页地址。下面咱们来为一个名为MyLib
的库的deprecatedMethod
方法添加该装饰器吧!class MyLib { @deprecate({ info: 'The methods will be deprecated in next version', url: 'http://www.baidu.com' }) deprecatedMethod(txt) { console.log(txt) } } const lib = new MyLib(); lib.deprecatedMethod('调用了一个要在下个版本被移除的方法'); // DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail // 调用了一个要在下个版本被移除的方法
3 总结
经过ESnext中的装饰器实现装饰器模式,不只有为类扩充功能的做用,并且在阅读源码的过程当中起到了提示做用。上面所举到的例子只是结合装饰器的新语法和装饰器模式作了一个简单封装,请勿用于生产环境。若是你如今已经体会到了装饰器模式的好处,并想在项目中大量使用,不妨看一下
core-decorators
这个库,其中封装了不少经常使用的装饰器.参考文献
来源:https://segmentfault.com/a/1190000015970099
<ul><li>在<code>webpack.config.js</code>中添加<code>babel-loader</code> </li></ul><p>若是你使用的IDE为Visual Studio Code,可能还须要在项目根目录下添加如下<code>tsconfig.json</code>文件来组织一个ts检查的报错。</p> <pre><code class="JSON">{ "compilerOptions": { "experimentalDecorators": true, "allowJs": true, "lib": [ "es6" ], } }@autobind@debounce@deprecate@autobindthisthisthisclass Person { getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // falsegetPersonthisPersonPersonthisundefinedfalseautobindgetPersonthisPersonfunction autobind(target, key, descriptor) { var fn = descriptor.value; var configurable = descriptor.configurable; var enumerable = descriptor.enumerable; // 返回descriptor return { configurable: configurable, enumerable: enumerable, get: function get() { // 将该方法绑定this var boundFn = fn.bind(this); // 使用Object.defineProperty从新定义该方法 Object.defineProperty(this, key, { configurable: true, writable: true, enumerable: false, value: boundFn }) return boundFn; } } }bindthisgetObject.definePropertyvaluebindboundFnthisgetPersonclass Person { @autobind getPerson() { return this; } } let person = new Person(); let { getPerson } = person; console.log(getPerson() === person); // true@debounceresizescrollclass Editor { constructor() { this.content = ''; } updateContent(content) { console.log(content); this.content = content; // 后面有一些消耗性能的操做 } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); // 打印结果: 1 3 2 4EditorupdateContent1 3 2 4debouncetimeoutfunction debounce(timeout) { const instanceMap = new Map(); // 建立一个Map的数据结构,将实例化对象做为key return function (target, key, descriptor) { return Object.assign({}, descriptor, { value: function value() { // 清除延时器 clearTimeout(instanceMap.get(this)); // 设置延时器 instanceMap.set(this, setTimeout(() => { // 调用该方法 descriptor.value.apply(this, arguments); // 将延时器设置为 null instanceMap.set(this, null); }, timeout)); } }) } }Mapdebouncedebounceclass Editor { constructor() { this.content = ''; } @debounce(500) updateContent(content) { console.log(content); this.content = content; } } const editor1 = new Editor(); editor1.updateContent(1); setTimeout(() => { editor1.updateContent(2); }, 400); const editor2= new Editor(); editor2.updateContent(3); setTimeout(() => { editor2.updateContent(4); }, 600); //打印结果: 3 2 4updateContent3 2 41400ms500@deprecate@deprecate@deprecate@log@fetchInfofunction deprecate(deprecatedObj) { return function(target, key, descriptor) { const deprecatedInfo = deprecatedObj.info; const deprecatedUrl = deprecatedObj.url; // 警告信息 const txt = `DEPRECATION ${target.constructor.name}#${key}: ${deprecatedInfo}. ${deprecatedUrl ? 'See '+ deprecatedUrl + ' for more detail' : ''}`; return Object.assign({}, descriptor, { value: function value() { // 打印警告信息 console.warn(txt); descriptor.value.apply(this, arguments); } }) } }deprecateinfourlinfourlMyLibdeprecatedMethodclass MyLib { @deprecate({ info: 'The methods will be deprecated in next version', url: 'http://www.baidu.com' }) deprecatedMethod(txt) { console.log(txt) } } const lib = new MyLib(); lib.deprecatedMethod('调用了一个要在下个版本被移除的方法'); // DEPRECATION MyLib#deprecatedMethod: The methods will be deprecated in next version. See http://www.baidu.com for more detail // 调用了一个要在下个版本被移除的方法core-decorators