JavaScript设计模式(一)

145 阅读9分钟

本系列文章介绍针对JavaScript的设计模式,本章介绍单例模式、策略模式、代理模式、迭代器模式和中介者模式

三类设计模式

  • 创建型模式
    • 这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
  • 结构型模式
    • 这类模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
  • 行为模式
    • 这类模式负责对象间的高效沟通和职责委派。

1. 单例模式

单例模式是创建型设计模式, 保证一个类仅有一个实例,并提供一个访问它的全局访问点

实现

实现一个单例模式并不复杂,无非就是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则下次获取该类实例时直接返回之前创建的对象

实现一个单例模式,它的作用是负责在页面中创建唯一的div节点

用代理实现单例模式

 // 实现一个单例模式,它的作用是负责在页面中创建唯一的div节点
 // 这是一个类
 var CreateDiv = function (html) {
   this.html = html;
   this.init();
 };
 ​
 CreateDiv.prototype.init = function () {
   var div = document.createElement("div");
   div.innerHTML = this.html;
   document.body.appendChild(div);
 };
 ​
 var ProxySingletonCreateDiv = (function () {
   var instance;
   return function (html) {
     if (!instance) {
       instance = new CreateDiv(html);
     }
     return instance;
   };
 })();
 ​
 var a = new ProxySingletonCreateDiv("sven1");
 var b = new ProxySingletonCreateDiv("sven2");
 ​
 alert(a === b);

这段代码的注意点有两个

  1. CreateDiv是一个类,init()是类方法,但在JS这样的无类语言中也许我们可以不需要这样一个类
  2. 代理的作用是,CreateDiv是一个普通的类,我们既可以用它生产多个实例,也可以和代理组合起来以达到单例的效果

JS中通用的惰性单例

单例模式的核心思想是确保只有一个实例,并提供全局访问

 // 单例模式的核心逻辑
 var getSingle = function (fn) {
   var result;
   return function () {
     return result || (result = fn.apply(this, arguments));
   };
 };
 ​
 var someMethod = function () {
   return Symbol(1);
 };
 ​
 var createSingleMethod = getSingle(someMethod);
 ​
 console.log(someMethod() === someMethod())   // false
 ​
 console.log(createSingleMethod() === createSingleMethod())   // true

在这个例子中,someMethod要做的职责和管理单例的职责分别放在两个方法里(职责单一),这两个方法可以独立变化而不互相影响,当它们组合在一起的时候,就完成了创建唯一实例对象的功能。

优点

  • 你可以保证一个类只有一个实例。
  • 你获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。

2. 策略模式

策略模式是一种行为模式,定义一系列的算法(业务),把它们一个个封装起来,并且使它们可以相互替换。

也就是将算法的使用与算法的实现分离开来

使用

基于策略模式的程序至少由两部分组成:策略类环境类

  • 策略类封装了具体的算法,负责具体计算过程
  • 环境类接受请求,并将请求委托给某一个策略类

使用策略模式可以消除原程序中大片的条件分支语句

实现

公司每月按照S、A、B三个等级发奖金,每个等级奖金不同,我们当然也可以用分支语句实现,但这样的实现包含了很多逻辑分支,而且一旦要增加C等级,则需要深入原函数改写其内部实现,违反了开放-封闭原则,复用性也很差。

let 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
calculateBonus("S", 6000); // 输出:24000

改用策略类实现,将算法的实现和算法的使用分离开来,将算法放入策略类中,具体应该使用哪个算法,由环境类决定

// 策略类
var strategies = {
  S: function (salary) {
    return salary * 4;
  },
  A: function (salary) {
    return salary * 3;
  },
  B: function (salary) {
    return salary * 2;
  },
};

// 环境类
var calculateBonus = function (level, salary) {
  return strategies[level](salary);
};

console.log(calculateBonus("S", 20000));
console.log(calculateBonus("A", 20000));

优点

  1. 利用组合、委托和多态等技术思想,可以有效避免多重条件选择语句
  2. 提供来对开放-封闭原则的完美支持,将算法独立封装,使得它们易于切换、易于理解、易于扩展
  3. 算法可复用在系统的其他地方

3. 代理模式

代理模式是一种结构型设计模式,为一个对象提供一个代用品或占位符,以便控制对它的访问

保护代理和虚拟代理

保护代理

代理 B 可以帮助 A 过滤掉一些请求,A 只用专心于自己的业务

虚拟代理(在JS中更常用)

把一些开销很大的对象,延迟到真正需要它的时候才去创建,比如

  • 虚拟代理实现图片懒加载,在图片加载好之前用一个占位图片代替
  let myImage = (function () {
    let imgNode = document.createElement('img')
    document.body.appendChild(imgNode)

    return {
      setSrc: function (src) {
        imgNode.src = src
      }
    }
  })()

  let proxyImage = (function () {
    let img = new Image
    img.onload = function () {
      myImage.setSrc(this.src)
    }
    return {
      setSrc: function (src) {
        myImage.setSrc('./loading.jpg')
        img.src = src
      }
    }
  })()

  proxyImage.setSrc('https://img1.baidu.com/it/u=359638371,2587777304&fm=253&fmt=auto&app=120&f=JPEG?w=690&h=388')

这段代码符合开放-封闭原则,给img设置src和图片预加载两个功能在两个对象里,可以各自变化而不影响对方,如果我们不需要预加载了,只需要修改请求本体而不是代理即可。这样做降低了程序的耦合度

  • 虚拟代理合并HTTP请求
    • 如果有个button,我们每点击一次向服务器发送一次请求,快速的点击会在短时间内发送大量请求,增加了服务器的压力。那我们可以将在某一时间内的请求,如2s内的请求收集起来,一次性发送,减少请求次数,减轻服务器压力

代理和本体接口的一致性

应保证代理和本体接口的一致性

  • 用户可以放心请求代理,而不必知道代理和本体的区别,只关心得到的结果
  • 任何使用本体的地方都可以使用代理

缓存代理

可以为一些开销大的运算结果提供暂时的存储

// 
let mult = function (...args) {
  console.log("计算乘积");
  let a = 1;
  for (let i = 0; i < args.length; i++) {
    a *= args[i];
  }
  return a;
};

console.log(mult(2, 3));
console.log(mult(2, 3, 4, 5));

let proxyMult = (function () {
  let cache = {};
  return function (...args) {
    let arg = args.join(',')
    if (arg in cache) {
      return cache[arg];
    }
    cache[arg] = mult.apply(this, args)
    return cache[arg];
  };
})();

console.log(proxyMult(1, 2, 3, 4, 5, 6));
// 第二次执行时就不用重复计算,而是从缓存里面拿
console.log(proxyMult(1, 2, 3, 4, 5, 6));

通过增加缓存代理,mult 函数依然专注于自己的职责——计算乘积,而缓存的功能由代理对象实现

4. 迭代器模式

迭代器模式是一种行为设计模式,提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示

大多语言都有各自的迭代器实现,即便如此,我们也可以学习一下迭代器模式的实现。

迭代器模式好比我们逛游乐园,游乐项目都是固定不变的,但我们有很多种游玩方式。可以对照着地图路线逛,可以跟着导游走,也可以走到哪玩到哪。游乐项目好比一个集合,游玩方式是我们的迭代方式,我们可以选择倒序迭代,正序迭代等

内部迭代器和外部迭代器

内部迭代器

函数内部已经定义好了迭代规则,完全接手整个迭代过程,外部只需要一次初始调用

  • 外界不用关心迭代器内部的实现,跟迭代器的交互也仅是一次初始调用
  • 迭代规则已经确定,灵活度不及外部迭代器
let each = function (arr, callback) {
  for (let i = 0, l = arr.length; i < l; i++) {
    // 把下标和元素当做参数传给callback函数
    callback.call(arr[i], i, arr[i]);
  }
};

each([1, 2, 3], function (i, n) {
  console.log([i, n]);
});

内部迭代器

  • 必须显示地请求迭代下一个元素
  • 增加了一些调用的复杂度,但也增强了迭代器的灵活性
  • 可以手动控制迭代的过程或顺序
let Iterator = function (obj) {
  let current = 0;

  let next = function () {
    current += 1;
  };

  let isDone = function () {
    return current >= obj.length;
  };

  let getCurrentItem = function () {
    return obj[current];
  };

  return {
    next,
    isDone,
    getCurrentItem,
    length: obj.length,
  };
};

// 判断两迭代对象是否相等
let compare = function (iterator1, iterator2) {
  if (iterator1.length !== iterator2.length) {
    console.log("iterator1 和 iterator2 不相等");
    return;
  }
  // 手动递归
  while (!iterator1.isDone() && !iterator2.isDone()) {
    if (iterator1.getCurrentItem() !== iterator2.getCurrentItem()) {
      throw new Error("iterator1 和 iterator2 不相等");
    }
    iterator1.next();
    iterator2.next();
  }
  console.log("iterator1 和 iterator2 相等");
};

let iterator1 = Iterator([1, 2, 3, 4]);
let iterator2 = Iterator([1, 2, 3, 4]);

compare(iterator1, iterator2);

5. 中介者模式

中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

当程序的规模增大,对象会越来越多,它 们之间的关系也越来越复杂,难免会形成网状的交叉引用,当我们改变或删除其中一个对象的时候,很可能需要通知所有引用到它的对象

image.png

中介者模式的作用就是解除对象与对象之间的紧耦合关系,通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合

image.png

现实中的中介者

中介者就好比机场的指挥塔,如果没有指挥塔,每一架飞机起飞降落前都要和周围的所有飞机通信才能确定航线和飞行情况,这肯定是很混乱的。而塔台作为调停者,知道每一架飞机的飞行情况,可以安排所有飞机的起降时间和航线。

image.png

适用场景

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
    • 该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。
  • 当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。
    • 应用中介者模式后, 每个组件不再知晓其他组件的情况。 尽管这些组件无法直接交流, 但它们仍可通过中介者对象进行间接交流。 如果你希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。
  • 如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。
    • 由于所有组件间关系都被包含在中介者中, 因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。

中介者模式的优缺点

优点

  • 单一职责原则。 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。
  • 开闭原则。 你无需修改实际组件就能增加新的中介者。
  • 你可以减轻应用中多个组件间的耦合情况。
  • 你可以更方便地复用各个组件。

缺点

  • 系统中会新增一个中介者对象,对象之间交互的复杂性转移成了中介者对象的复杂性。中介者对象自身往往就是一个难以维护的对象

模块之间有一些依赖关系很正常,我们应该衡量对象之间的耦合程度,再考虑是否要使用中介者对象

参考书籍

  1. JavaScript设计模式与开发实践 —— 曾探
  2. refactoringguru.cn/ 一个很好的学习设计模式的网站

感谢阅读~