一些JavaScript设计模式

设计模式: 对特定问题简单且优雅的解决方案.

设计模式的应用场景并不是相互对立的, 同一个问题运用不同的设计模式解决时代码上可能有较大的重复, 我们应该关注的是某个模式可以应用于什么场景, 解决什么问题.

设计模式并不一定需要手动实现, 很多语言自带了一些设计模式实现的特性. 可以说学习设计模式实际上体现了编程语言在某些方向的设计缺陷与不足

设计模式之于 JavaScript

  • JavaScript 是动态类型语言, 我们可以直接调用某个对象上的方法与属性而不需要提前检测对象的类型.
  • JavaScript 的类型是鸭子类型, 即不关注对象是不是某个类型的(IS-A)而关注对象有没有某些特征(HAS-A)

多态

JavaScript 鸭子类型意味着无需向上转型即可直接实现多态, 我们可以在函数实现上实现多态, 也可以在传入对象上实现多态

// 在函数实现上实现多态
function makeSound(bar){
  if(bar.type === 'duck')return console.log('GaGaGa');
  if(bar.type === 'chicken')return console.log('GuGuGu');
  return false
}

makeSound(new Duck()) // GaGaGa
makeSound(new Chicken()) // GuGuGu

// 在传入对象上实现多态
function makeSound(bar){
  return bar.sound && bar.sound()
}

makeSound(new Duck()) // GaGaGa
makeSound(new Chicken()) // GuGuGu

多态背后的思想是将 "做什么" 与 "谁去做, 如何做" 分开, 显然第二份代码更优雅, 更有弹性.

在实现多态时我们也要注意如何将 "做什么"(不变的, makeSound)"谁去做, 如何做"(变化的, 动物发出什么) 两者分开

封装

可以借助作用域隐藏私有变量

function Chicken() {
  let language = 'GuGuGu'; // 私有
  this.sound = () => console.log(language); // 公有
}
new Chicken().sound();


const duck = (function () {
  let language = 'GaGaGa'; // 私有
  return {
    sound: () => console.log(language),
  };
})();
duck.sound();

原型模式

JavaScript 选择了基于原型的面向对象系统, JavaScript 并没有类的概念. 在创建对象时, JavaScript 不会找对象属于什么类, 而是会寻找对象对应的原型, 然后克隆这个原型对象得到目标对象.

可以这样对比, 假设我们需要制造一支红色的笔

  • 基于类的面向对象系统: 找到 Pen 类型, 为构造函数传入 color = 'red', 构造对象. (类更像是一个模子, 对象就是这个模子浇铸出来的铸件)
  • 基于原型的面向对象系统: 找到 Pen 对应的原型对象, 克隆这个对象, 得到一根笔, 将颜色改为红色. (原型对象就像一个没有分化的细胞 (基础的, 默认的对象), 创建对象就是让细胞分裂一次, 分裂出的细胞可能不符合要求, 我们可以让新细胞继续分化, 使得其符合要求)

JavaScript 利用原型链实现了方法的继承与委托

JavaScript 创建对象的原理

JavaScript 中的函数既可以当作函数也可以当作构造器使用, 通过 new 创建对象时, 函数会作为构造器参与构造. 如果构造器返回的对象, 那么该对象会作为 new 的结果取代克隆的对象

JavaScript 会先克隆构造器对应的原型对象(绑定 __proto__), 然后对对象执行构造器函数, 最后检查构造器函数的返回值决定返回哪个对象作为构造结果

function newOperator(ctor, ...params) {
  // 校验构造函数
  if (typeof ctor !== 'function') throw ctor + 'is not a constructor';
  // 设置 new.target
  newOperator.target = ctor;
  // 克隆原型
  var rtn_obj = Object.create(ctor.prototype);
  // 传入参数、绑定this、获取构造函数返回的结果
  var instance = ctor.apply(rtn_obj, params);
  // 判断构造函数返回的类型
  if (
    (typeof instance === 'object' && instance !== null) ||
    typeof instance === 'function'
  )
    return instance;
  return rtn_obj;
}

单例模式

保证类只有一个实例, 这个实例全局可见

在 JavaScript 中实现单例的简单方法就是将对象附着在 global, 但是这会污染全局作用域. 虽然可以采用诸如命名空间的方法规避污染问题, 但是这种单例模式过于简易

JavaScript 的通用惰性单例

function getSingleFn(fn) {
  let instance;
  return function () {
    return instance || (instance = fn.apply(this, arguments)); // 只有调用时才执行函数
  };
}

测试

getData = getSingleFn(() => Date.now());

setInterval(() => {
  console.log(Date.now());
  console.log(getData());
}, 1000);

其他应用

惰性单例不仅保证了全局唯一, 还保证了传入函数只运行一次, 可以实现类似 once 功能

策略模式

当待解决的问题需要通过大量 "可替代" 的 "算法" 实现时可以考虑策略模式

应用场景

假设某个插值函数 lerp(time, st, ed, during, method) 提供了很多插值选项 (line / ease-in / ease-out...) 如果要在一个函数中实现所有功能, 那函数就变为了

function lerp(time, st, ed, during, method) {
  if (method === 'line') return /* ... */;
  else if (method === 'ease-in') return /* ... */;
  else if (method === 'ease-out') return /* ... */;
  // ...
}

这样的函数缺乏弹性且违反了开闭原则: 当我们需要新增一个插值模式时, 我们需要再增加一个 if-else

可以发现, 这个函数的代码中涉及了很多平行的 if-else, 每个分支内部的算法很类似且目标相同(可替代) 可以考虑策略模式

静态类型的策略模式

定义一个策略类与环境类 (Context), 将每个算法封装为一个策略类, 将数据配置到环境类, 在需要计算时调用环境类请求配置时指定的策略类

// 策略类

class LineLerp {
  calculate(time, st, ed, during) {
    /* ... */
  }
}

class EaseinLerp {
  calculate(time, st, ed, during) {
    /* ... */
  }
}

class EaseoutLerp {
  calculate(time, st, ed, during) {
    /* ... */
  }
}

// 环境类

class Lerp {
  setEnvs(time, st, ed, during, method) {
    this.time = time;
    this.st = st;
    this.ed = ed;
    this.during = during;
    this.method = method; // 策略类, 这里需要向上转型一下
  }
  calculate() {
    return this.method.calculate(this.time, this.st, this.ed, this.during); // 环境类调用策略类
  }
}

// 测试
const lerp1 = new Lerp(); // 创建环境类
lerp1.setEnvs(0.1, 1, 2, 2, new LineLerp()); // 配置环境类
lerp1.calculate(); // 计算

JavaScript 的策略模式

可以定义一个对象存储所有策略

// 策略类
const lerps = {
  line: (time, st, ed, during) => {/* ... */},
  easein: (time, st, ed, during) => {/* ... */},
  easeout: (time, st, ed, during) => {/* ... */},
};

// 环境类
class Lerp {
  constructor(time, st, ed, during, method) {
    this.time = time;
    this.st = st;
    this.ed = ed;
    this.during = during;
    this.method = method; // 策略
  }
  calculate() {
    return lerps[this.method](this.time, this.st, this.ed, this.during); // 环境类调用策略类
  }
}

// 测试
const lerp1 = new Lerp(0.1, 1, 2, 2, 'line'); // 创建环境类
lerp1.calculate(); // 计算

其他应用场景

实际上只要我们的算法业务目标一致, 具有可替代性就可以利用策略模式

例如: 表单验证

// 定义策略
const strategies = {
  isNotEmpty: (value = '', msg) => value.trim() === '' && msg,
  minLength: (value = '', minLen, msg) => value.length < minLen && msg,
  isMobile: (value = '', msg) => !/^1\d{10}$/.test(value) && msg,
};

// 定义环境类
class Validator {
  constructor() {
    this.cache = [];
  }

  add(value, rules) { // 添加策略, rule = 策略:参数...
    rules.forEach((d) => {
      const { strategy, errorMsg } = d;
      const [rule, ...params] = strategy.split(':');
      this.cache.push(strategies[rule].bind(null, value, ...params, errorMsg));
    });
  }

  start() {
    for (let i of this.cache) {
      const msg = i();
      if (msg) console.log(msg);
    }
  }
}

const userInfo = {
  userName: 'hihi',
  passWord: 'hi',
  address: ' ',
  tel: '222',
};

const validator = new Validator();
validator.add(userInfo.userName, [ { strategy: 'minLength:2', errorMsg: 'Username >2!' } ]);
validator.add(userInfo.passWord, [ { strategy: 'minLength:4', errorMsg: 'pwd >4!' } ]);
validator.add(userInfo.address, [ { strategy: 'isNotEmpty', errorMsg: 'add nn!' } ]);
validator.add(userInfo.tel, [
  { strategy: 'isNotEmpty', errorMsg: 'tel nn!' },
  { strategy: 'isMobile', errorMsg: 'NOT A TEL!'},
]);

validator.start();

代理模式

提供一个代用品以控制外部的访问

实现代理的原则

  • 透明代理: 代理 API 设计的与原 API 一样, 当我们不需要代理的时候可以直接移除代理代码而不需要大改 API

保护代理

相当于一个防火墙, 每次触发时有选择的执行或阻止触发事件

虚拟代理

将性能开销大的事务延迟到可用时执行

  • 图片预加载

    class MyImage {
      constructor() {
        this.imageNode = document.createElement('img');
        document.body.append(this.imageNode);
      }
    
      setSrc(src) {
        this.imageNode.setAttribute('src', src);
      }
    }
    
    function setImageProxy(target, src, srcLoad = 'http://预加载图片地址') {
      const img = new Image(); // 临时图像对象
      img.onload = () => target.setSrc(src); // 当临时图像加载完成后为目标赋值
      img.src = src; // 要求临时图像加载
      target.setSrc(srcLoad); // 目标图像先放 loading 图
    }
    
    const imgNode = new MyImage();
    setImageProxy(imgNode, 'http://大图地址');

  • 合并请求

    对于少量多次的请求可以做请求合并

    function getFetchProxy(interval) {
      const cache = [];
      let timer;
      return function (v) {
        cache.push(v);
        if (timer) return;
        timer = setTimeout(() => {
          clearTimeout(timer);
          timer = null;
          fetch('http://example.com/' + cache.join(','));
          cache.length = 0;
        }, interval);
      };
    }
    
    const urlProxy = getFetchProxy(2000);
    urlProxy(1)
    urlProxy(2)
  • 惰性加载

    类似单例模式的惰性加载

    function getFetchProxy() {
    const cache = [];
    return function (v) {
      if (v) return cache.push(v);
      fetch('http://example.com/' + cache.join(','));
      cache.length = 0;
    };
    }
    
    const urlProxy = getFetchProxy();
    urlProxy(1);
    urlProxy(2);
    urlProxy();

缓存代理

缓存请求参数即可

function getMultiProxy() { // 计算乘法的代理
  const cache = new Map();
  return function (...args) {
    const k = args.join(' ');
    if (!cache.has(k))
      cache.set(
        k,
        args.reduce((pre, cur) => pre * cur, 1)
      );
    return cache.get(k);
  };
}

其他代理

  • 防火墙代理
  • 远程代理: 代理其他内存区域中的数据
  • 保护代理: 实现权限控制
  • 智能引用代理: 在指针基础上提供了一些回调方法
  • 写时复制代理: 惰性复制, 只有要修改原对象时才赋值

迭代器模式

  • 内部迭代器: 迭代器位于函数内部, 只需要传入对每个对象的调用方法
  • 外部迭代器: 需要显式操作迭代器(next, isDone)

发布订阅模式

又称观察者模式, 用于维护对象之间的一对多关系, 一旦对象状态发生改变, 所有依赖(订阅)于这个对象的对象都会得到消息

当某个对象需要在其他对象发生变化后变化, 最简单的实现方式是 RR, 但这势必会造成硬编码与强耦合. 我们希望采用类似 DOM 中 addEventListener 的方法实现事件注册并在触发后自动执行. 模仿这个过程就可以得到发布订阅模式与观察者模式.

JavaScript 的观察者模式

观察者模式中存在观察者与被观察者, 观察者发生变化后通知被观察者

//观察者类
class Observer {
  constructor() {}
  //观测到变化后的处理
  update(ob) {
    console.log(ob);
  }
}
class Observed {
  constructor() {
    this.observers = [];
  }
  //添加观察者
  addObserver(observer) {
    this.observers.push(observer);
  }
  //删除观察者
  removeObserver(observer) {
    this.observers = this.observers.filter((o) => o != observer);
  }
  //通知所有的观察者
  notify() {
    this.observers.forEach((observer) => observer.update(this));
  }
}

使用

class Teacher extends Observer {
  constructor(name) {
    super();
    this.name = name;
  }
  update(st) {
    console.log(st.name + `提交了${this.name}作业`);
  }
}
class Student extends Observed {
  constructor(name) {
    super();
    this.name = name;
  }
  submitHomeWork() {
    this.notify(this);
  }
}

const students = [1, 2, 3].map((d) => new Student(d));
const teacher = new Teacher('t');
teacher.update(students[1]); // 2提交了t作业

JavaScript 发布订阅模式

在订阅者模式中, 当被观察者发生变化时被观察者会直接通知所有观察者. 在消息订阅模式中, 消息发布者(被观察者)可以自定义发布消息类型, 只有订阅了此类消息的人才会收到消息.

非全局消息中心

function installEvent(sender) {
  sender.eventType = new Map();

  sender.listen = function (event, fn) {
    if (!sender.eventType.has(event)) sender.eventType.set(event, []); // 不能用 set 万一有重复就寄了
    const fns = sender.eventType.get(event);
    fns.push(fn);
  };

  sender.trigger = function (event, ...args) {
    if (!sender.eventType.has(event)) return;
    const fns = sender.eventType.get(event);
    fns.forEach((d) => d(...args));
  };

  sender.cancel = function (event, fn) {
    if (!sender.eventType.has(event)) return;
    const fns = sender.eventType.get(event);
    const idx = fns.findIndex((d) => d === fn);
    ~idx && fns.splice(idx, 1);
  };
}

使用

const sender = {};
installEvent(sender);
const fn = () => console.log('ev1 #1');
sender.listen('ev1', fn);
sender.listen('ev1', () => console.log('ev1 #2'));
sender.listen('ev2', () => console.log('ev2 #1'));

sender.trigger('ev1'); // ev1 #1 ev1 #2
console.log('---');
sender.trigger('ev2'); // ev2 #1
console.log('---');
sender.cancel('ev1', fn);
sender.trigger('ev1'); // ev1 #2

有消息缓存的全局消息中心

我们发现订阅者者要在注册事件时需要显式的指定发布者, 这也造成了一定程度的耦合, 我们可以定义一个全局消息中心, 订阅者向消息中心请求订阅消息, 消息发布者发布消息到消息中心, 由消息中心通知订阅者.

const Event = (function () {
  const eventType = new Map();
  const cache = new Map();

  function listen(event, fn) {
    if (!eventType.has(event)) eventType.set(event, []); // 不能用 set 万一有重复就寄了
    const fns = eventType.get(event);
    fns.push(fn);
    retrieveCache(event);
  }

  function trigger(event, ...args) {
    if (!eventType.has(event)) return addCache(...arguments);
    const fns = eventType.get(event);
    if (!fns.length) return addCache(...arguments);
    fns.forEach((d) => d(...args));
  }

  function cancel(event, fn) {
    if (!eventType.has(event)) return;
    const fns = eventType.get(event);
    const idx = fns.findIndex((d) => d === fn);
    ~idx && fns.splice(idx, 1);
  }

  function addCache(event, ...args) {
    if (!cache.has(event)) cache.set(event, []);
    cachedArgs = cache.get(event);
    cachedArgs.push(args);
  }

  function retrieveCache(event) {
    if (!cache.has(event)) return;
    cachedArgs = cache.get(event);
    cachedArgs.forEach((d) => trigger(event, ...d));
    cache.set(event, []);
  }

  return { listen, trigger, cancel };
})();

使用

const sender = {};
const fn = () => console.log('ev1 #1');
Event.listen('ev1', fn);
Event.listen('ev1', () => console.log('ev1 #2'));
Event.listen('ev2', () => console.log('ev2 #1'));
Event.trigger('ev1'); // ev1 #1 ev1 #2
console.log('---');
Event.trigger('ev2'); // ev2 #1
console.log('---');
Event.cancel('ev1', fn);
Event.trigger('ev1'); // ev1 #2
console.log('---');
Event.trigger('ev3', 1, 2, 3, 4);
Event.listen('ev3', (...args) =>
  console.log('ev3 #1, ans = ' + args.reduce((pre, cur) => pre + cur, 0))
); // ev3 #1, ans = 10

应用场景

某个对象改变, 使依赖于它的多个对象得到通知, 且希望解耦两者. 发布-订阅模式适合更复杂的场景(发布者的某次更新只想通知它的部分订阅者)

命令模式

当我们不清楚请求的接受者是谁也不知道请求的具体操作是什么. 我们可以将命令封装为对象, 让命令对象在程序中被四处传递

命令模式的三个组成部分: 发起者(不知道自己触发的命令有啥用, 也不知道谁最终接收命令) 命令(一个被拿来拿去的对象) 传递/接收者(只管传递/接收命令)

可以类比餐厅点餐:

  • 命令: 一个包含了如何做菜的指令对象
  • 发起者: 客户, 客户发起做饭的命令, 但是客户也不知道做饭的命令最终接受者是谁, 他只是发出了一个做菜命令
  • 传递者: 服务员, 服务员将客户发出的做菜指令传递给大厨
  • 执行者: 大厨, 大厨只管执行命令, 他也不知道是谁点的餐, 他只负责执行

可以看到, 在这里我们完全做到了发起者, 命令, 接受者的解耦

静态类型语言的命令模式

// 为发起者绑定命令的方法
function setCommandDOM(dom, command) {
  dom.addEventListener('click', () => command.execute());
}

// 实际功能的实现
const showTimes = {
  showYear: () => new Date().getFullYear(),
  showMonth: () => new Date().getMonth() + 1,
  showDay: () => new Date().getDay(),
};

// 命令类
function ShowCurrentYearCommand(receiver) { this.receiver = receiver; }
ShowCurrentYearCommand.prototype.execute = function () {
  this.receiver.innerText = showTimes.showYear();
};

function ShowCurrentAllCommand(receiver) { this.receiver = receiver; }
ShowCurrentAllCommand.prototype.execute = function () {
  this.receiver.innerText =
    '' + showTimes.showYear() + showTimes.showMonth() + showTimes.showDay();
};

function ClearAllCommand(receiver) { this.receiver = receiver; }
ClearAllCommand.prototype.execute = function () {
  this.receiver.innerText = '';
};

// 构造命令(命令 -> 接收者, 接收者不知道是谁发起的)
const showCurrentYearCommand = new ShowCurrentYearCommand(
  document.getElementById('content')
);
const showCurrentAllCommand = new ShowCurrentAllCommand(
  document.getElementById('content')
);
const clearAllCommand = new ClearAllCommand(
  document.getElementById('content')
);

// 发起者发起命令(发起者 -> 命令, 发起者也不知道这命令是干啥的)
setCommandDOM(document.getElementById('btn1'), showCurrentYearCommand);
setCommandDOM(document.getElementById('btn2'), showCurrentAllCommand);
setCommandDOM(document.getElementById('btn3'), clearAllCommand);
  • 实现命令功能
  • 实现命令类保存 context 实现 execute
  • 构造实例对象, 完成 命令 -> 执行者的绑定
  • 将命令绑定给发起者

撤销执行: 将指令调用保存到栈, 需要撤销时候弹指令即可. 同时在指令上定义 undo 方法实现单条指令的撤销(因为每条指令只需要实现 execute 的逆, 不用考虑全局变化, 所以不算麻烦), 例如

function ClearAllCommand(receiver) {
  this.receiver = receiver;
  this.last = '';
}
ClearAllCommand.prototype.execute = function () {
  this.last = this.receiver.innerText;
  this.receiver.innerText = '';
};
ClearAllCommand.prototype.undo = function () {
  this.receiver.innerText = this.last;
};

宏命令

可以将多个命令组合起来形成一个宏命令, 执行宏命令等于执行宏命令下的每个命令

function buildMacroCommand() {
  const commandList = [];
  return {
    add(command) {
      commandList.push(command);
    },
    execute() {
      commandList.forEach((d) => d.execute());
    },
    undo() {
      [...commandList].reverse().forEach((d) => d.undo && d.undo());
    },
  };
}

我们尽量让宏命令和普通命令 API 保持一致, 这样可以保证宏命令对发起者透明

智能命令与傻瓜命令

命令执行与执行者无关的称为智能命令(例如 alert()), 和执行者有关的(例如 receiver.innerText)

应用场景

  • 不清楚请求的接受者
  • 不知道请求的具体操作是什么
  • 命令要被四处传递四处传递

命令模式与策略模式在形式上有点类似, 但是命令模式的命令传递更加灵活, 且实现了命令发起者与命令的解耦. 在 JavaScript 中, 命令可以是一个函数, 但是函数在 JavaScript 中是可以随意传递的, 所以 JavaScript 天生就直接支持命令模式

组合模式

采用尽可能相同的接口定义对象, 并使对象具有嵌套能力. 这样我们就可以通过对象的嵌套实现功能的组合.

应用: 宏命令就采用组合模式的思想, 宏命令与普通命令的 API 相同(形式一致), 宏命令可以嵌套宏命令与普通命令, 也可以通过 execute 执行自己包含的子命令(功能一致). 命令调用者无需区分宏命令和普通命令, 只需要无脑调用 execute 即可.

应用场景: 组合模式将对象以部分-整体的模式组成树形结构, 并要求部分与整体具有功能与形式的一致性

特性: 称这种模式为组合模式是因为不同对象之间可以方便组合拆分对象, 快速形成一个树形结构, 而调用者只需要关注树根即可调用整个对象, 且不需要关系调用的对象是嵌套对象还是基本对象

注意: - 嵌套对象与基本对象接口设计不可能完全一致, 如果用户对基本对象调用嵌套对象会造成引用错误, 可以尝试在基本对象上定义相同 API 并抛出错误 - 组合模式的对象应该是严格的树形结构, 如果出现环, 就意味着我们可能无法控制环中对象调用次数, 如果产生了环可以考虑采用策略 / 责任链模式解决问题

模板方法模式

在抽象类中定义算法的宏观运行模式与可复用的子算法, 由子类实现具体每一步的算法

静态类型的模板方法模式

实现制作饮料算法, 其中

  • 制作咖啡: 开水 -> 泡咖啡粉 -> 倒入杯子 -> 加糖
  • 制作奶茶: 开水 -> 泡茶 -> 倒入杯子 -> 加牛奶

可以抽象制作饮料的算法: 开水 -> 泡水 -> 倒入杯子 -> 加料

实现制作奶茶

class MakeDrink {
  constructor() {
    this.water = { temp: 0, consist: [] };
    this.cup = null;
  }

  // 公共算法提前实现
  boilWater() {
    this.water.temp = 100;
  }

  // 私有算法不实现
  brew() {
    throw new Error('需要实现算法');
  }

  pourCup() {
    this.cup = this.water;
  }

  addCondiments() {
    throw new Error('需要实现算法');
  }

  execute() {
    this.boilWater();
    this.brew();
    this.pourCup();
    this.addCondiments();
    return this.cup;
  }
}

class MakeMilkTea extends MakeDrink {
  brew() {
    this.water.consist.push('tea');
  }

  addCondiments() {
    this.water.consist.push('milk');
  }
}

const makeMilkTea = new MakeMilkTea();
console.log(makeMilkTea.execute()); // { temp: 100, consist: [ 'tea', 'milk' ] }

我们还可以在实现抽象类的时候加入钩子函数, 实现流程控制, 例如: 默认加配料, 但是支持使用钩子函数传入是否加料

class MakeDrink {
  // ...

  needCondiments() { // 默认加料
    return true;
  }

  execute() {
    this.boilWater();
    this.brew();
    this.pourCup();
    if (this.needCondiments()) this.addCondiments();
    return this.cup;
  }
}

class MakeCoffee extends MakeDrink {
  constructor(need) {
    super();
    this.need = need;
  }

  brew() {
    this.water.consist.push('coffee');
  }

  addCondiments() {
    this.water.consist.push('sugar');
  }

  needCondiments() {
    return this.need;
  }
}

const makeCoffee = new MakeCoffee(false);
console.log(makeCoffee.execute()); // { temp: 100, consist: [ 'coffee' ] }

const makeCoffeeWithSugar = new MakeCoffee(true);
console.log(makeCoffeeWithSugar.execute()); // { temp: 100, consist: [ 'coffee', 'sugar' ] }

JavaScript 的模板方法模式

class MakeDrink {
  constructor(params) {
    this.water = { temp: 0, consist: [] };
    this.cup = null;

    // 先尽力去取
    this.methods = params;

    // 公共算法内部实现, 如果以及提供就不实现
    this.methods.boilWater ??= () => {
      this.water.temp = 100;
    };

    // 私有算法不实现
    this.methods.brew ??= () => {
      throw new Error('需要实现算法');
    };

    this.methods.pourCup ??= () => {
      this.cup = this.water;
    };

    this.methods.addCondiments ??= () => {
      throw new Error('需要实现算法');
    };

    this.methods.needCondiments ??= () => {
      return true;
    };

    for (let k in this.methods) {
      this.methods[k] = this.methods[k].bind(this);
    }
  }

  execute() {
    this.methods.boilWater();
    this.methods.brew();
    this.methods.pourCup();
    if (this.methods.needCondiments()) this.methods.addCondiments();
    return this.cup;
  }
}

const makeMilkTea = new MakeDrink({
  brew() {
    this.water.consist.push('tea');
  },
  addCondiments() {
    this.water.consist.push('milk');
  },
});

console.log(makeMilkTea.execute()); // { temp: 100, consist: [ 'tea', 'milk' ] }

const makeCoffee = new MakeDrink({
  brew() {
    this.water.consist.push('coffee');
  },

  addCondiments() {
    this.water.consist.push('sugar');
  },

  needCondiments() {
    return true;
  },
});
console.log(makeCoffee.execute()); // { temp: 100, consist: [ 'coffee', 'sugar' ] }

const makeCoffeeWithSugar = new MakeDrink({
  brew() {
    this.water.consist.push('coffee');
  },

  addCondiments() {
    this.water.consist.push('sugar');
  },
});
console.log(makeCoffeeWithSugar.execute()); // { temp: 100, consist: [ 'coffee', 'sugar' ] }

享元模式

运用共享技术尽可能的复用对象

将对象上的属性分为内部属性与外部属性

  • 内部属性属于对象内部, 可以被外部共享
  • 内部属性独立于场景, 不会随场景变化变化(在共享过程中保持不变)
  • 外部属性取决于应用场景, 不会随场景变化而变化

对象剥离外部对象成为共享对象, 在使用时传入外部状态组成对象

应用场景

  • 相似元素多次调用
  • 元素体积较大, 多次创建会造成较大开销
  • 多数状态为外部状态
  • 同时使用对象量小, 可以用少量对象完成大多数场景

使用 JavaScript 实现对象池

享元模式只是一种思想, 没有具体的实现, 对象池是享元模式的一种应用

let objPoolFactory = function (createObjFn) {
  let objPool = [];
  return {
    create() {
      if (objPool.length) {
        return objPool.shift();
      }
      return createObjFn.apply(this, arguments);
    },
    recover(obj) {
      objPool.push(obj);
    },
  };
};

职责链模式

请求者发起请求, 将请求传递给中间人, 中间人一直传递直到遇到一个可处理者

很类似于拦截器

JavaScript 的职责链模式

function processState400(code) {
  if (code >= 400 && code < 500) console.log('error 400');
  else return this.next(code);
}

function processState500(code) {
  if (code >= 500 && code < 600) console.log('server 500');
  else return this.next(code);
}

function processState200(code) {
  if (code >= 200 && code < 300) console.log('ok 200');
  else return this.next(code);
}

function processStateUnknown(code) {
  console.log('unknown code');
}

class Chain {
  constructor(fn) {
    this.fn = fn;
    this.successor = null;
  }

  setNextSuccessor(fn) {
    return (this.successor = new Chain(fn));
  }

  next() {
    return this.successor && this.successor.execute(...arguments);
  }

  execute() {
    return this.fn(...arguments);
  }
}

const processCode = new Chain(processState200);
processCode
  .setNextSuccessor(processState400)
  .setNextSuccessor(processState500)
  .setNextSuccessor(processStateUnknown);

processCode.execute(210);
processCode.execute(310);
processCode.execute(410);
processCode.execute(510);

利用 AOP 实现职责链

Function.prototype.after = function (fn) {
  const self = this;
  return function () {
    const res = self.apply(this, arguments);
    if (res === 'nextSuccessor') return fn.apply(this, arguments);
    return res;
  };
};

function processState400(code) {
  if (code >= 400 && code < 500) console.log('error 400');
  else return 'nextSuccessor';
}

function processState500(code) {
  if (code >= 500 && code < 600) console.log('server 500');
  else return 'nextSuccessor';
}

function processState200(code) {
  if (code >= 200 && code < 300) console.log('ok 200');
  else return 'nextSuccessor';
}

function processStateUnknown(code) {
  console.log('unknown code');
}

const processCode = processState200
  .after(processState400)
  .after(processState500)
  .after(processStateUnknown);

processCode(210);
processCode(310);
processCode(410);
processCode(510);

与组合模式相比, 职责链模式可以手动指定执行的起点, 职责修改更灵活

中介者模式

中介者模式与发布订阅模式类似, 都是实现解决内部状态改变引发外部行为改变.

  • 发布订阅模式只支持发布消息, 消息中心触发订阅者对应事件. 消息是单向传递的
  • 中介者模式的中介者功能更加强大, 中介者拥有智能处理事件的能力(不仅可以传递消息, 还可以在收到消息后做一系列动作). 同时, 中介者模式不区分发布者与订阅者, 所有对象都是中介服务的对象, 对象之间平等.

在中介者模式中, 中心的工作能力大大增强, 所以中介者模式也被称为调停者模式

JavaScript 的中介者模式

命令定义类似策略模式, 接口定义类似发布订阅模式. 实现一个玩家中介, 玩家可以换队伍, 同色队伍全死后对方队伍成员收消息.

// 中介者模式
function Player(name, teamColor) {
  this.state = 'live'; // 玩家状态
  this.name = name;
  this.teamColor = teamColor; //队伍颜色
}

Player.prototype.win = function () {
  console.log(this.name + ': won');
};

Player.prototype.lose = function () {
  console.log(this.name + ': lost');
};

// 玩家死亡
Player.prototype.die = function () {
  this.state = 'dead';
  //给中介者发消息,玩家死亡
  playerDirector.reciveMessage('playerDead', this);
};

// 移除玩家
Player.prototype.remove = function () {
  playerDirector.reciveMessage('removePlayer', this);
};

// 玩家换队
Player.prototype.changeTeam = function (color) {
  playerDirector.reciveMessage('changeTeam', this, color);
};

let playerFactory = function (name, teamColor) {
  let newPlayer = new Player(name, teamColor);
  playerDirector.reciveMessage('addPlayer', newPlayer);
  return newPlayer;
};

let playerDirector = (function () {
  let players = {}, // 保存所有玩家
    operations = {}; // 中介者可以执行的操作

  operations.addPlayer = function (player) {
    let teamColor = player.teamColor;
    players[teamColor] = players[teamColor] || []; // 如果还没成立队伍,就新成立一个队伍
    players[teamColor].push(player); // 加入一个玩家
  };

  operations.removePlayer = function (player) {
    let teamColor = player.teamColor;
    teamPlayers = players[teamColor] || []; //该队伍所有玩家
    players[teamColor] = teamPlayers.filter((item) => item !== player); //移除玩家
  };

  operations.changeTeam = function (player, newTeamColor) {
    operations.removePlayer(player); // 从原来队伍移除
    player.teamColor = newTeamColor; // 改变自身的队伍颜色
    operations.addPlayer(player); // 添加到新队伍
  };

  operations.playerDead = function (player) {
    let teamColor = player.teamColor,
      teamPlayers = players[teamColor]; //玩家所在队伍

    let all_dead = true;

    for (let i = 0, player; (player = teamPlayers[i++]); ) {
      if (player.state !== 'dead') {
        all_dead = false;
        break;
      }
    }

    if (all_dead === true) {
      // 全部死亡
      for (let i = 0, player; (player = teamPlayers[i++]); ) {
        player.lose(); // 所有玩家失败
      }
      for (let color in players) {
        if (color !== teamColor) {
          let teamPlayers = players[color];
          // 找出其他队伍玩家
          for (let i = 0, player; (player = teamPlayers[i++]); ) {
            player.win(); // 其他队伍胜利
          }
        }
      }
    }
  };

  let reciveMessage = function () {
    let message = [].shift.call(arguments);
    operations[message].apply(this, arguments);
  };

  return {
    reciveMessage,
  };
})();

let player1 = playerFactory('小明', 'red'),
  player2 = playerFactory('小乖', 'red'),
  player3 = playerFactory('小宏', 'red');

let player4 = playerFactory('小白', 'blue'),
  player5 = playerFactory('小黑', 'blue'),
  player6 = playerFactory('小牛', 'blue');
// player1.die();
// player3.die();
// player2.die();
// 小明: lost
// 小乖: lost
// 小宏: lost
// 小白: won
// 小黑: won
// 小牛: won

// player1.remove();
// player3.die();
// player2.die();
// 小乖: lost
// 小宏: lost
// 小白: won
// 小黑: won
// 小牛: won

player1.changeTeam('blue');
player2.die();
player3.die();
// 小乖: lost
// 小宏: lost
// 小白: won
// 小黑: won
// 小牛: won
// 小明: won

缺陷

我们引入消息订阅中心是为了方便实现信息交流, 避免硬编码与因实现通信导致的对象臃肿. 但是随着消息中心功能变强, 逐渐演化为中介者, 中介者对象也会变得越来越臃肿

装饰者模式

装饰者模式又称包装器模式, 就是将一个函数包装在另一个函数里面.

JavaScript 的装饰者模式

JavaScript 支持高阶函数, 所以天生支持装饰者模式. 例如, 我们想为 window.onload 绑定内容但不知道 window.onload 是否以及绑定了函数, 我们可以写

const _load = window.onload;

window.onload = function () {
  _load && _load();
  // 我们自己想绑定的
};

使用 AOP 实现装饰器模式

Function.prototype.before = function (fn) {
  const self = this;
  return function () {
    fn.apply(this, arguments);
    return self.apply(this, arguments);
  };
};

Function.prototype.after = function (fn) {
  const self = this;
  return function () {
    const res = self.apply(this, arguments);
    fn.apply(this, arguments);
    return res;
  };
};

const f1 = () => console.log(1);
const f2 = () => console.log(2);

const f = f1.after(f2).before(f2);
f(); // 2 1 2

应用场景

装饰器模式可以为函数动态的加入新的职责与行为, 当一个函数的具体功能无法在运行前确定时或需要为函数加入非主线任务的工作(AOP)时可以考虑装饰者模式. 同时, 考虑到其可以客观将代码分开, 当我们想要将不同层级代码从同一个函数中分离时也可以考虑这一模式(例如在表单提交中将合法性检查与提交请求与 DOM 刷新分开, 者也可以看作是一种 AOP)

状态模式

将事务的每种状态都封装为类, 与当前状态相关的行为被直接封装到状态类中, 而在调用者看来, 对象似乎可以根据状态的不同调整行为

假设我们要实现一个支持三档切换的电灯(关-弱光-强光)

// 定义状态类
class OffLight {
  constructor(light) {
    this.light = light;
  }

  // 不同状态在按开关时行为不同
  switch() {
    this.light.setState(this.light.weakLight);
    console.log('变为弱光');
  }
}

class WeakLight {
  constructor(light) {
    this.light = light;
  }

  switch() {
    this.light.setState(this.light.strongLight);
    console.log('变为强光');
  }
}

class StrongLight {
  constructor(light) {
    this.light = light;
  }

  switch() {
    this.light.setState(this.light.offLight);
    console.log('熄灯了');
  }
}

// 定义上下文类
class Light {
  constructor() {
    // 手写每一种状态
    this.offLight = new OffLight(this); // new 语句中 this 指向构造对象
    this.weakLight = new WeakLight(this); // new 语句中 this 指向构造对象
    this.strongLight = new StrongLight(this); // new 语句中 this 指向构造对象
    this.button = null;
    this.curState = this.offLight;
  }

  setButton(dom) {
    this.button = dom;
    this.button.addEventListener('click', () => this.switch());
  }

  setState(state) {
    this.curState = state;
  }

  switch() {
    this.curState.switch();
  }
}

new Light().setButton(document.getElementById('switch'));

可以看到上下文中包含了所有状态, 关联行为统一定义在状态中, 上下文类只负责调用

使用有限状态机实现状态模式

var delegate = function (client, delegation) {
  return {
    buttonWasPressed: function () {
      // 将客户的操作委托给delegation 对象
      return delegation.buttonWasPressed.apply(client, arguments);
    },
  };
};

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log('关灯');
      this.button.innerHTML = '下一次按我是开灯';
      this.currState = this.onState;
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log('开灯');
      this.button.innerHTML = '下一次按我是关灯';
      this.currState = this.offState;
    },
  },
};

var Light = function () {
  this.offState = delegate(this, FSM.off);
  this.onState = delegate(this, FSM.on);
  this.currState = this.offState; // 设置初始状态为关闭状态
  this.button = null;
};

Light.prototype.init = function () {
  var button = document.createElement('button'),
    self = this;
  button.innerHTML = '已关灯';
  this.button = document.body.appendChild(button);
  this.button.onclick = function () {
    self.currState.buttonWasPressed();
  };
};
var light = new Light();
light.init();

优缺点

  • 状态及其对应的行为被封装在状态里, 非常方便后期新增状态与状态转换
  • 通过独立状态类避免了 context 类变臃肿
  • 使用对象表示状态比用字符串更一目了然
  • 不同状态之间的行为独立

优化点

  • 动态创造 / 销毁状态而不是全部定义在上下文类中
  • 共享状态

适配器模式

当两个部分代码由于接口不同不兼容时可以采用适配器模式将不兼容的接口转为兼容接口