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

2018-08-11 admin

1 什么是装饰器模式

Decorator

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

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

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

2 ESnext中的装饰器模式

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

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

npm install babel-core babel-loader babel-plugin-transform-decorators babel-plugin-transform-decorators-legacy babel-preset-env
{
  "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

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-37770.html

文章标题:从ES6重新认识JavaScript设计模式: 装饰器模式

相关文章
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
YouTube正式默认使用HTML5视频播放器
YouTube视频网站现在默认使用HTML5播放器,这意味着更好的性能、 稳定性、 电池寿命和甚至是更好的安全性。现在用户通过Chrome、IE 11、Safari 8和Beta版本的Firefox进行浏览的时候都默认使用HTML5视频播放...
2015-11-12
JavaScript常用特效chm下载
下载地址:JavaScript常用特效chm下载 对了,如果打开空白,在手册上右键属性解除锁定即可。 ...
2015-11-12
从2014年的发展来展望JS的未来将会如何
<font face="寰�杞�闆呴粦, Arial, sans-serif ">2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
破解前端面试(80% 应聘者不及格系列):从 闭包说起
不起眼的开始 招聘前端工程师,尤其是中高级前端工程师,扎实的 JS 基础绝对是必要条件,基础不扎实的工程师在面对前端开发中的各种问题时大概率会束手无策。在考察候选人 JS 基础的时候,我经常会提供下面这段代码,然后让候选人分析它实际运行的结...
2017-06-02
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
javascript是什么意思
avaScript是Netscape开发的一个对象脚本语言,它使用在世界各地数以百万计的网页和服务器应用程序上。 网景的JavaScript是ecma - 262版的标准脚本语言,和公布的标准只有轻微的差异。 与广为流行的错误理解相反,Ja...
2015-11-12
AJAX的浏览器支持
AJAX 的要点是 XMLHttpRequest 对象。 不同的浏览器创建 XMLHttpRequest 对象的方法是有差异的。 IE 浏览器使用 ActiveXObject,而其他的浏览器使用名为 XMLHttpRequest 的 Jav...
2015-11-12
21天学通javascript
简介: 本书是Javascript入门教程。Javascript是Web开发中应用最早、发展最成熟、用户最多的脚本语言。其语法简洁,代码可读性在众多脚本语言中最好,它在使用时不用考虑数据类型,是真正意义上的动态语言。本书总分为四篇,共21章...
2015-11-16
Node.js学习(1)----HTTP服务器与客户端
Node.js 标准库提供了 http 模块,其中封装了一个高效的 HTTP 服务器和一个简易的HTTP 客户端。http.Server 是一个基于事件的 HTTP 服务器,它的核心由 Node.js 下层 C++部分实现,而接口由 Jav...
2015-11-12
回到顶部