1 什么是装饰器模式

向一个现有的对象添加新的功能,同时又不改变其结构的设计模式被称为装饰器模式(Decorator Pattern),它是作为现有的类的一个包装(Wrapper)。

可以将装饰器理解为游戏人物购买的装备,例如LOL中的英雄刚开始游戏时只有基础的攻击力和法强。但是在购买的装备后,在触发攻击和技能时,能够享受到装备带来的输出加成。我们可以理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。

这里推荐一篇淘宝前端团队的博文,很有趣的以钢铁侠的例子来讲解了装饰者模式。

2 ESnext中的装饰器模式

ESnext中有一个Decorator提案,使用一个以 @ 开头的函数对ES6中的class及其属性、方法进行修饰。Decorator的详细语法请参考阮一峰的《ECMASciprt入门 —— Decorator》

目前Decorator的语法还只是一个提案,如果期望现在使用装饰器模式,需要安装配合babel + webpack并结合插件实现。

  • npm安装依赖

npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
  • 配置.babelrc文件
{
"presets": ["env"],
"plugins": ["transform-decorators-legacy"]
}
```
  • webpack.config.js中添加babel-loader

module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
],
}

如果你使用的IDE为Visual Studio Code,可能还需要在项目根目录下添加以下tsconfig.json文件来组织一个ts检查的报错。

{
"compilerOptions": {
"experimentalDecorators": true,
"allowJs": true,
"lib": [
"es6"
],
}
}
``` 下面我将实现3个装饰器,分别为@autobind@debounce@deprecate

2.1 @autobind实现this指向原对象

在JavaScript中,this的指向问题一直是一个老生常谈的话题,在Vue或React这类框架的使用过程中,新手很有可能一不小心就丢失了this的指向导致方法调用错误。例如下面一段代码:

class Person {
getPerson() {
return this;
}
} let person = new Person();
let { getPerson } = person; console.log(getPerson() === person); // false

上面的代码中, getPerson方法中的this默认指向Person类的实例,但是如果将Person通过解构赋值的方式提取出来,那么此时的this指向为undefined。所以最终的打印结果为false

此时我们可以实现一个autobind的函数,用来装饰getPerson这个方法,实现this永远指向Person的实例。


function 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)在前端项目中有着很多的应用,例如在resizescroll等事件中操作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 41由于在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函数接受一个对象参数,该参数分别有infourl两个键值,其中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这个库,其中封装了很多常用的装饰器.

参考文献

  1. IMWeb的前端博客:浅谈JS中的装饰器模式
  2. 淘宝前端团队:ES7 Decorator 装饰者模式
  3. 阮一峰:ECMAScript 6 入门

来源:https://segmentfault.com/a/1190000015970099

从ES6重新认识JavaScript设计模式: 装饰器模式的更多相关文章

  1. JavaScript设计模式—装饰器模式

    装饰器模式介绍 为对象添加新的功能,不改变其原有的结构和功能,原有的功能还是可以使用,跟适配器模式不一样,适配器模式原有的已经不能使用了,装饰器示例比如手机壳 UML类图和代码示例 Circle示原来 ...

  2. JAVA设计模式--装饰器模式

    装饰器模式 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构.这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装. 这种模式创建了一个装饰 ...

  3. 从ES6重新认识JavaScript设计模式(三): 建造者模式

    1 什么是建造者模式? 建造者模式(Builder)是将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示. 建造者模式的特点是分步构建一个复杂的对象,可以用不同组合或顺序建造出不 ...

  4. C#设计模式--装饰器模式

    0.C#设计模式-简单工厂模式 1.C#设计模式--工厂方法模式 2.C#设计模式--抽象工厂模式 3.C#设计模式--单例模式 4.C#设计模式--建造者模式 5.C#设计模式--原型模式 6.C# ...

  5. C#设计模式-装饰器模式(Decorator Pattern)

    引言 当我们完成一个软件产品开发后就需要对其进行各种测试,适配快速迭代下质量的保障.当有一个完善的产品的对象后,如果我们想要给他添加一个测试功能,那么我们可以用一个新的类去装饰它来实现对原有对象职责的 ...

  6. 设计模式-装饰器模式(Decrator Model)

    文 / vincentzh 原文连接:http://www.cnblogs.com/vincentzh/p/6057666.html 目录 1.概述 2.目的 3.结构组成 4.实现 5.总结 1.概 ...

  7. php设计模式 装饰器模式

    装饰器模式,可以动态地添加修改类的功能. 一个类提供了一项功能,如果要修改并添加额外的功能,传统的编程模式需要写一个子类继承它,并重新实现类的方法.使用装饰器模式,仅需要在运行时添加一个装饰器对象即可 ...

  8. 说说设计模式~装饰器模式(Decorator)

    返回目录 装饰器模式,也叫又叫装饰者模式,顾名思义,将一个对象进行包裹,包装,让它变成一个比较满意的对象,这种模式在我们平时项目开发中,经常会用到,事实上,它是处理问题的一种技巧,也很好的扩展了程序, ...

  9. 说说设计模式~装饰器模式(Decorator)~多功能消息组件的实现

    返回目录 为何要设计多功能消息组件 之前写过一篇装饰器模式的文章,感觉不够深入,这次的例子是实现项目中遇到的,所以把它拿出来,再写写,之前也写过消息组件的文章,主要采用了策略模式实现的,即每个项目可以 ...

随机推荐

  1. web页面调用app的方法

    use_app_goto_page: (skip_type, skip_target) => { // Android App if (/android/i.test(navigator.use ...

  2. ZROI 19.07.30 简单图论/kk

    1.最短路 NOI2019 D2T1 我被这题送Fe了/lb 只有zz才会写二维线段树,比如我. 实际上你只需要矩形取min就可以. kd-tree可以随便过,最慢的点\(0.1s\). 另外一种简单 ...

  3. vue组件样式scoped

    1.vue组件中的样式如果没加scrped,样式代表的是全局样式(避免组件之间样式的冲突).加了属性代表是模块化的. 其他组件引用button组件 上面分析了单个组件渲染后的结果,那么组件互相调用之后 ...

  4. cron常用表达式

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11905247.html 推荐一个cron表达式生成的网站:https://www.freeformat ...

  5. 6398. 【NOIP2018模拟10.30】Generator(树状数组区间修改)

    题目描述 Description Input Output 输出 q 行,第 i 行表示数据 Di 的答案. Sample Input 4 3 2 1 1 2 4 2 1 2 1 1 3 5 2 2 ...

  6. 整合spring cloud云架构 - 根据token获取用户信息

    根据用户token获取yoghurt信息的流程: /** * 根据token获取用户信息 * @param accessToken * @return * @throws Exception */ @ ...

  7. Linux内核设计与实现 总结笔记(第十五章)进程地址空间

    一.地址空间 进程地址空间由进程可寻址的虚拟内存组成,内核允许进程使用这种虚拟内存中的地址. 每个进程都有一个32位或64位的平坦地址空间,空间的具体大小取决于体系结构.“平坦”指的是地址空间范围是一 ...

  8. asp.net 怎么上传文件夹啊,不传压缩包

    最近遇见一个需要上传百兆大文件的需求,调研了七牛和腾讯云的切片分段上传功能,因此在此整理前端大文件上传相关功能的实现. 在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表 ...

  9. Angular项目 Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed.报错

    在angular的项目里,一不小心就会出现这个错误[ngRepeat:dupes] ,这个问题是因为内容有重复引起的解决起来挺简单 在对应的ng-repeat指令中增加track by $index, ...

  10. 电脑新安装JDK版本并运行使用该JDK版本问题

    情景:电脑上已正常安装一个jdk版本,如:1.7.0_71,因考虑到一些情况,现需要使用版本为1.7.0_80(1.8),故需新安装JDK,并使服务可以运行使用新安装的JDK版本. 网络找寻方法: ( ...