什么是装饰者模式

装饰者模式是一种为函数或类增添特性的技术,它可以让我们在不修改原来对象的基础上,为其增添新的能力和行为。它本质上也是一个函数(在javascipt中,类也只是函数的语法糖)。

我们什么时候可以弄到它呢

我们来假设一个场景,一个自行车商店有几种型号的自行车,现在商店允许用户为每一种自行车提供一些额外的配件,比如前灯、尾灯、铃铛等。每选择一种或几种配件都会影响自行车的售价。

如果按照比较传统的创建子类的方式,就等于我们目前有一个自行车基类,而我们要为每一种可能的选择创建一个新的类。可是由于用户可以选择一种或者几种任意的配件,这就导致最终可能会生产几十上百个子类,这明显是不科学的。然而,对这种情况,我们可以使用装饰者模式来解决这个问题。

自行车的基类如下:

class Bicycle {    // 其它方法    wash () {}    ride () {}    getPrice() {        return 200;    }}

那么我们可以先创建一个装饰者模式基类

class BicycleDecotator {    constructor(bicycle) {        this.bicycle = bicycle;    }    wash () {        return this.bicycle.wash();    }    ride () {        return this.bicycle.ride();    }    getPrice() {        return this.bicycle.getPrice();    }}

这个基类其实没有做什么事情,它只是接受一个Bicycle实例,实现其对应的方法,并且将调用其方法返回而已。

有了这个基类之后,我们就可以根据我们的需求对原来的Bicycle类为所欲为了。比如我可以创建一个添加了前灯的装饰器以及添加了尾灯的装饰器:

class HeadLightDecorator extends BicycleDecorator {    constructor(bicycle) {        super(bicycle);    }    getPrice() {        return this.bicycle.getPrice() + 20;    }}class TailLightDecorator extends BicycleDecorator {    constructor(bicycle) {        super(bicycle);    }    getPrice() {        return this.bicycle.getPrice() + 20;    }}

那么,接下来我们就可以来对其自由组合了:

let bicycle = new Bicycle();console.log(bicycle.getPrice()); // 200bicycle = new HeadLightDecorator(bicycle); // 添加了前灯的自行车console.log(bicycle.getPrice());  // 220bicycle = new TailLightDecorator(bicycle); // 添加了前灯和尾灯的自行车console.log(bicycle.getPrice()); // 240

这样写的好处是什么呢?假设说我们有10个配件,那么我们只需要写10个配件装饰器,然后就可以任意搭配成不同配件的自行车并计算价格。而如果是按照子类的实现方式的话,10个配件可能就需要有几百个甚至上千个子类了。

从例子中我们可以看出装饰者模式的适用场合:

  1. 如果你需要为类增添特性或职责,可是从类派生子类的解决方法并不太现实的情况下,就应该使用装饰者模式。
  2. 在例子中,我们并没有对原来的Bicycle基类进行修改,因此也不会对原有的代码产生副作用。我们只是在原有的基础上增添了一些功能。因此,如果想为对象增添特性又不想改变使用该对象的代码的话,则可以采用装饰者模式。

装饰者模式除了可以应用在类上之外,还可以应用在函数上(其实这就是高阶函数)。比如,我们想测量函数的执行时间,那么我可以写这么一个装饰器:

function func() {    console.log('func');}function timeProfileDecorator(func) {    return function (...args) {        const startTime = new Date();        func.call(this, ...args);        const elapserdTime = (new Date()).getTime() - startTime.getTime();        console.log(`该函数消耗了${elapserdTime}ms`);    }}const newFunc = timeProfileDecorator(func);console.log(newFunc());

做一些有趣的事情

既然知道了装饰者模式可以在不修改原来代码的情况下为其增添一些新的功能,那么我们就可以来做一些有趣的事情。

我们可以为一个类的方法提供性能分析的功能。

class TimeProfileDecorator {  constructor(component, keys) {    this.component = component;    this.timers = {};    const self = this;    for (let i in keys) {      let key = keys[i];        if (typeof component[key] === 'function') {          this[key] = function(...args) {            this.startTimer(key);            // 解决this引用错误问题            component[key].call(component, ...args);            this.logTimer(key);          }        }    }  }  startTimer(namespace) {    this.timers[namespace] = new Date();  }  logTimer(namespace) {    const elapserdTime = (new Date()).getTime() - this.timers[namespace].getTime();    console.log(`该函数消耗了${elapserdTime}ms`);  }}// exampleclass Test {  constructor() {    this.name = 'cjg';    this.age = 22;  }  sayName() {    console.log(this.name);  }  sayAge() {    console.log(this.age);  }}let test1 = new Test();test1 = new TimeProfileDecorator(test1, ['sayName', 'sayAge']);console.log(test1.sayName());console.log(test1.sayAge());

对函数进行增强

节流函数or防抖函数

function throttle(func, delay) {    const self = this;    let tid;    return function(...args) {        if (tid) return;        tid = setTimeout(() => {            func.call(self, ...args);            tid = null;        }, delay);    }}function debounce(func, delay) {    const self = this;    let tid;    return function(...args) {        if (tid) clearTimeout(tid);        tid = setTimeout(() => {            func.call(self, ...args);            tid = null;        }, delay);    }}

缓存函数返回值

// 缓存函数结果,对于一些计算量比较大的函数效果比较明显。function memorize(func) {    const cache = {};    return function (...args) {        const key = JSON.stringify(args);        if (cache[key]) {          console.log('缓存了');          return cache[key];        }        const result = func.call(this, ...args);        cache[key] = result;        return result;    };}function fib(num) {  return num < 2 ? num : fib(num - 1) + fib(num - 2);}const enhanceFib = memorize(fib);console.log(enhanceFib(40));console.log(enhanceFib(40));console.log(enhanceFib(40));console.log(enhanceFib(40));

构造React高阶组件,为组件增加额外的功能,比如为组件提供shallowCompare功能:

import React from 'react';const { Component } = react;const ShadowCompareDecorator = (Instance) => class extends Component {  shouldComponentUpdate(nextProps, nextState) {    return !shallowCompare(this.props, nextProps) ||      !shallowCompare(this.state, nextState);  }  render() {    return (      <Instance {...this.props} />    );  }};export default ShadowCompareDecorator;

当然,你如果用过react-redux的话,你肯定也用过connect。其实connect也是一种高阶组件的方式。它通过装饰者模式,从Provider的context里拿到全局的state,并且将其通过props的方式传给原来的组件。

总结

使用装饰者模式可以让我们为原有的类和函数增添新的功能,并且不会修改原有的代码或者改变其调用方式,因此不会对原有的系统带来副作用。我们也不用担心原来系统会因为它而失灵或者不兼容。就我个人而言,我觉得这是一种特别好用的设计模式。

一个好消息就是,js的装饰器已经加入了es7的草案里啦。它让我们可以更加优雅的使用装饰者模式,如果有兴趣的可以添加下babel的plugins插件提前体验下。阮一峰老师的这个教程也十分浅显易懂。

参考文献:

Javascript设计模式