前言

平时的开发中可能不太需要用到设计模式,但是 JS 用上设计模式对于性能优化和项目工程化也是很有帮助的,下面就对常用的设计模式进行简单的介绍与总结。

1. 单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。


class Singleton {

  constructor(age) {

    this.age = age;

  }



  static getInstance(age) {

    const instance = Symbol.for(\'instance\'); // 防止被覆盖

    if (!Singleton[instance]) {

        Singleton[instance] = new Singleton(age);

    }

    return Singleton[instance];

  }

}



const singleton = Singleton.getInstance(30);

const singleton2 = Singleton.getInstance(20);

console.log(singleton === singleton2); // true

2. 策略模式

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

策略模式的核心是整个分为两个部分:

  • 第一部分是策略类,封装具体的算法;

  • 第二部分是环境类,负责接收客户的请求并派发到策略类。

现在我们假定有这样一个需求,需要对表现为S、A、B的同事进行年终奖的计算,分别对应为4倍、3倍、2倍工资,常见的写法如下:


const calculateBonus = function (performanceLevel, salary) {

  if (performanceLevel === \'S\') {

    return salary * 4;

  }

  if (performanceLevel === \'A\') {

    return salary * 3;

  }

  if (performanceLevel === \'B\') {

    return salary * 2;

  }

};



calculateBonus(\'B\', 20000); // 40000

可以看到,代码里面有较多的 if else 判断语句,如果对应计算方式改变或者新增等级,我们都需要对函数内部进行调整,且薪资算法重用性差,于是我们可以通过策略模式来进行重构,代码如下:


// 解决魔术字符串

const strategyTypes = {

  S: Symbol.for(\'S\'),

  A: Symbol.for(\'A\'),

  B: Symbol.for(\'B\'),

};

// 策略类

const strategies = {

  [strategyTypes.S](salary) {

    return salary * 4;

  },

  [strategyTypes.A](salary) {

    return salary * 3;

  },

  [strategyTypes.B](salary) {

    return salary * 2;

  }

};

// 环境类

const calculateBonus = function (level, salary) {

  return strategies[level](salary);

};



calculateBonus(strategyTypes.S, 300); // 1200

策略模式的优点:

  • 利用组合、委托、多态等技术和思想,有效地避免了多重 if-else 语句;

  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换、理解、扩展;

  • strategy 中的算法也可以用在别处,避免许多复制粘贴;

缺点:

  • 增加许多策略类或策略对象;

  • 违反知识最少原则;

3. 代理模式

定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

3.1 虚拟代理

在程序世界里,操作可能是昂贵的,这时候 B 通过监听 C 的状态来将 A 的请求发送过去,减少开销。

代理的意义

单一职责: 就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。

例子:图片预加载。

const myImage = (function () {
    const imgNode = document.createElement(\'img\'); 
    document.body.appendChild(imgNode);
    return function (src) {
        imgNode.src = src;
    }
})();

const proxyImage = (function () {
    const img = new Image;
    img.  = function () {
        myImage(this.src);
    }
    return function (src) {
        myImage(\'./loading.gif\');
        img.src = src;
    }
})();

proxyImage(\'./test.jpg\');

这里的 myImage 只进行图片 src 的设置,其他代理的工作交给了 proxyImage 方法,符合单一职责原则。此外,也保证了代理和本体接口的一致性。

3.2 缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

例子:计算乘积,缓存 ajax 数据。

const mult = function () {
    let a = 1;
    for (let i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
    }
    return a;
}

const proxyMult = (function () {
    const cache = {};
    return function () {
    const args = Array.prototype.join.call(arguments, \',\'); 
    if (args in cache) {
        return cache[args];
    }
    return cache[args] = mult.apply(this, arguments);
    }
})();

const a = proxyMult(1, 2, 3, 4); // 输出:24
const b = proxyMult(1, 2, 3, 4); // 输出:24

4. 观察者模式

观察者模式又叫发布—订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 开发中,我们一般用事件模型来替代传统的观察者模式。

4.1 DOM 事件

最早接触到的观察者模式大概就是 DOM 事件了,比如用户的点击操作。我们没办法知道用户什么时候点击,但是当用户点击时,被点击的节点就会向订阅者发布消息。

document.body.addEventListener(\'click\', function() {
  alert(\'我被点击啦!~\');
});

4.2 自定义事件

要实现自定义事件,需要进行三步:

  1. 指定发布者;
  2. 给发布者添加一个缓存列表,用以通知订阅者;
  3. 遍历缓存列表依次触发存放在里面的订阅者的回调函数;
class Event {
  constructor() {
    this.eventListObj = {};
  }
  static getInstance() {
    const instance = Symbol.for(\'instance\');
    if (!Event[instance]) {
      Event[instance] = new Event();
    }
    return Event[instance];
  }
  listen(key, fn) {
    if (!this.eventListObj[key]) {
      this.eventListObj[key] = [];
    }
    // 订阅消息添加进缓存列表
    this.eventListObj[key].push(fn);
  }
  trigger(key, ...args) {
    const fns = this.eventListObj[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    fns.forEach((fn) => {
      fn.apply(this, args);
    });
  }
  remove(key, fn) {
    let fns = this.eventListObj[key];
    // 如果没有被订阅过
    if (!fns) {
      return false;
    }
    // 根据 fn 参数来判断是全部移除还是指定移除
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (let i = 0; i < fn.length; i++) {
        const f = fns[i];
        if (f === fn) {
          fns.splice(i, 1);
        }
      }
    }
  }
}
const event = Event.getInstance(); // 全局发布者
const add = function (a, b) {
  console.log(a + b);
}
const minus = function (a, b) {
  console.log(a - b);
}
event.listen(\'add\', add); // 订阅加法消息
event.listen(\'minus\', minus); // 订阅减法消息

event.trigger(\'add\', 1, 3); // 触发加法订阅消息
event.trigger(\'minus\', 3, 1); // 触发减法订阅消息

console.log(\'------- before remove add function:\');
console.log(event);

event.remove(\'add\', add); // 取消加法订阅事件
console.log(\'------- after remove add function:\');
console.log(event);

执行结果:

例子:ajax 请求登录后进行多种操作,以及在 vue 中 emit 和 on,node.js 中的 events

5. 模板方法模式

模板方法模式是一种只需使用继承就可以实现的非常简单的模式。

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。

子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

下面我们来举个例子——假如我们要泡一杯茶和一杯咖啡步骤如下:

  1. 把水煮沸
  2. 用沸水 ( 冲泡咖啡 / 浸泡茶叶 )
  3. 把 ( 咖啡 / 茶水 ) 倒进杯子
  4. 加糖和牛奶 / 加柠檬

很容易发现其中第一步是共有的,其他步骤大体一致,那么我们就可以使用模板方法来实现它。( 假如有人不想加糖和牛奶怎么办呢? )

// 抽象出饮料类用来表示咖啡和茶
class Beverage {
  init() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    if (this.customerWantsCondiments()) {
      this.addCondiments();
    }
  }
  // 第一步:把水煮沸
  boilWater(){
    console.log(\'把水煮沸\');
  }
  // 第二步:冲泡饮料,在子类中重写
  brew(){
    throw new Error(\'brew function must override in child\');
  }
  // 第三步:倒出饮料,在子类中重写
  pourInCup(){
    throw new Error(\'pourInCup function must override in child\');
  }
  // 第四步:个性化饮料,在子类中重写
  addCondiments(){
    throw new Error(\'addCondiments function must override in child\');
  }
  // 钩子方法: 解决了有人不想加糖和牛奶的问题
  customerWantsCondiments() {
    return true;
  }
}

class Coffee extends Beverage {
  brew(){
    console.log(\'用沸水冲泡咖啡\');
  }
  pourInCup(){
    console.log(\'把咖啡倒进杯子\');
  }
  addCondiments(){
    console.log(\'加糖和牛奶\');
  }
  //  不想个性化
  customerWantsCondiments() {
    return false;
  }
}

classTea extends Beverage {
  brew(){
    console.log(\'用沸水浸泡茶叶\');
  }
  pourInCup(){
    console.log(\'把茶水倒进杯子\');
  }
  addCondiments(){
    console.log(\'加柠檬\');
  }
}

new Coffee().init();
new Tea().init();

6. 职责链模式

职责连模式:通过把对象连成一条链,让请求沿着这条链传递,直到有一个对象能处理为止,解决了发送者和接收者之间的耦合。

A --> B --> C --> … --> N,中间有一个对象能处理 A 对象的请求,如果没有需要在最后处理异常。

现实中的例子:早高峰挤公交的时候递公交卡,只需要往前递,总会递到售票员手里刷卡,而不用管递给了谁。

下面举一个实际的例子来看看——假如现在有个电商定金优惠券功能,付 500 元定金可以获得 100 元优惠券且一定能买到商品;付 200 元定金可以获得 50 元优惠券且一定能买到商品,如果付定金只能进入普通购买,需要在库存足够的时候才可以买到商品。我们顶一个一个函数,接收三个参数:

  • orderType:1、2、3 分表代表 500 元定金, 200 元定金和无定金模式;
  • pay:true、false 代表拍下订单是否付款;
  • stock:number 代表库存余量;
const order = function (orderType, pay, stock) {
  if (orderType === 1) {
    if (pay === true) {
      console.log(\'获得 100 元优惠券\');
    } else {
      if (stock > 0) {
        console.log(\'普通购买, 无优惠券\');
      } else {
        console.log(\'库存不足\');
      }
    }
  } else if (orderType === 2) {
    if (pay === true) {
      console.log(\'获得 50 元优惠券\');
    } else {
      if (stock > 0) {
        console.log(\'普通购买, 无优惠券\');
      } else {
        console.log(\'库存不足\');
      }
    }
  } else if (orderType === 3) {
    if (stock > 0) {
      console.log(\'普通购买, 无优惠券\');
    } else {
      console.log(\'库存不足\');
    }
  }
}
order(1, true, 20); // 获得 100 元优惠券

这显然不是一段好代码,大量的 if else 条件分支,如果业务再复杂一点,最后根本就没法看了。

那么我们通过 AOP 实现职责链:

const order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    return console.log(\'已支付定金,获得100元优惠券\');
  }
  return \'NEXT\';
}

const order200 = function (orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    return console.log(\'已支付定金,获得50元优惠券\');
  }
  return \'NEXT\';
}

const orderNormal = function (orderType, pay, stock) {
  if (stock > 0) {
    console.log(\'普通购买,无优惠券\');
  } else {
    console.log(\'库存不足\');
  }
}

Function.prototype.after = function (fn) {
  const self = this;
  return function (...args) {
    const result = self.apply(this, args);
    if (result === \'NEXT\') {
      return fn.apply(this, args);
    }
    return result;
  }
}

const order = order500.after(order200).after(orderNormal);
order(1, false, 10);

通过分解成三个独立的函数,返回处理不了的结果’NEXT’,交给下一个节点处理。通过 after 来进行绑定,最后我们在新增需求的时候可以在 after 中间插入即可,耦合度大大降低,但是这样也有一个不好的地方,职责链过长增加了函数的作用域。

7. 中介者模式

在程序里,对象经常会和其他对象进行通信,当项目比较大,对象很多的时候,这种通信就会形成一个通信网,当我们想要修改某一个对象时,需要十分小心,以免这些改动牵一发而动全身,导致出现BUG,非常的复杂。

中介者模式就是用来解除这些对象间的耦合,形成简单的对象到中介者到对象的操作。

下面以现实中的机场指挥塔为例说明。

  • 如果没有指挥塔的情况,每一架飞机都需要和其他飞机进行通信,确保航线的安全,我们假设目的地相同就为航线不安全:
// 飞机类
class Plane {
  constructor(name, to) {
    this.name = name;
    this.to = to;
    this.otherPlanes = []

					
				
收藏 打印
您的足迹: