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. 【leetcode】1175. Prime Arrangements

    题目如下: Return the number of permutations of 1 to n so that prime numbers are at prime indices (1-inde ...

  2. vue项目图片路径问题

    一般情况下我们为了能在本地显示效果,写图片路径会直接这样写,但是在实际中图片一般都是动态上传的, 所以,在vue中一般是这样的: 但是这样你会发现,图片根本显示不出来,只是显示了个图片的图标. 后来发 ...

  3. HDU 6616 Divide the Stones

    目录 题面 中文题意 比赛惨状 我的走不通的思路 \(m\)是偶数的情况 \(m\)是奇数的情况 题解的思路 另一些思路 源代码 题面 Time limit 3000 ms Memory limit ...

  4. Leetcode 12. Integer to Roman(打表,水)

    12. Integer to Roman Medium Roman numerals are represented by seven different symbols: I, V, X, L, C ...

  5. Codeforce |Educational Codeforces Round 77 (Rated for Div. 2) B. Obtain Two Zeroes

    B. Obtain Two Zeroes time limit per test 1 second memory limit per test 256 megabytes input standard ...

  6. [ethereum源码分析](3) ethereum初始化指令

    前言 在上一章介绍了关于区块链的一些基础知识,这一章会分析指令 geth --datadir dev/data/02 init private-geth/genesis.json 的源码,若你的eth ...

  7. Android流媒体开发之路三:基于NDK开发Android平台RTSP播放器

    基于NDK开发Android平台RTSP播放器 最近做了不少android端的开发,有推流.播放.直播.对讲等各种应用,做了RTMP.RTSP.HTTP-FLV.自定义等各种协议,还是有不少收获和心得 ...

  8. redux源码浅入浅出

    运用redux有一段时间了,包括redux-thunk和redux-saga处理异步action都有一定的涉及,现在技术栈转向阿里的dva+antd,好用得不要不要的,但是需要知己知彼要对react家 ...

  9. JS 弹出网页 (不显示地址栏,工具栏) 网页去掉地址栏

    JS 弹出网页 (不显示地址栏,工具栏) 网页去掉地址栏 window.open()支持环境: JavaScript1.0+/JScript1.0+/Nav2+/IE3+/Opera3+ 基本语法: ...

  10. Chromely

    Chromely Chromely is a lightweight alternative to Electron.NET, Electron for .NET/.NET Core develope ...