【设计模式】JavaScript设计模式

470 阅读10分钟

参考

概念

小明在玩云顶之奕时,研究出一种策略:前期买低费棋子打工,等升到 6 级后,疯狂刷新寻找暗星棋子,凑 6 暗星羁绊。他玩了很多把后,发现这个策略非常管用,基本每局都能进前 3。乐于分享的他将这个策略总结了下来,并放到网上与其他玩家分享。大家照着他的策略愉快地玩耍,久而久之,这种策略已然成为了一种固有套路。

程序员在写代码时,也有着一些固定的套路,按照某某套路写出来的代码,往往比较清晰、健壮。而这些套路,其实就是所谓的设计模式

创建型

构造器模式

// 你作为拳头的程序员
// 被要求用JavaScript重写LOL
// 首先,来一个快乐风男
const Yasuo = {
  name: "亚索",
  title: "疾风剑豪",
  location: "战士",
};
// 接下来,再来一个寒冰
const Ashe = {
  name: "艾希",
  title: "寒冰射手",
  location: "射手",
};
// 你想到,还有100多个英雄要写
// 如果继续这样写,还得再写一百多个对象字面量
// 不如写个构造函数吧
function Hero(name, title, location) {
  this.name = name;
  this.title = title;
  this.location = location;
}
// 好了,这样添加英雄省事多啦
const Jinx = new Hero("金克丝", "暴走萝莉", "射手");

简单工厂模式

// 设计师跟你说,不同定位的英雄应该有不同的特质
// 比如射手是移动的atm,刺客是ad去质器
// 于是你把Hero拆分成了Shooter和Assassin
function Shooter(name, title, location) {
  this.name = name;
  this.title = title;
  this.location = "射手";
  this.speciality = "移动的ATM";
}
function Assassin(name, skills) {
  this.name = name;
  this.title = title;
  this.location = "刺客";
  this.speciality = "AD去质器";
}
// 然后通过函数来判别英雄定位和分配构造函数
function handleLocation(name, title, location) {
  switch (location) {
    case "射手":
      return new Shooter(name, title, location);
      break;
    case "刺客":
      return new Assassin(name, title, location);
      break;
  }
}
const Caitlyn = handleLocation("凯特琳", "皮城女警", "射手");
const Zed = handleLocation("劫", "影流之主", "刺客");
// 但你思考了下,其实hooter和Assassin都有着共同的属性
// 而且location和speciality的值是统一的
// 即speciality的值是根据location的值决定的
// 为了更加彻底地封装共性和分离出个性,你又把他们整合回了Hero
function Hero(name, title, location, speciality) {
  this.name = name;
  this.title = title;
  this.location = location;
  this.speciality = speciality;
}
// 然后写一个生成Hero的函数
// 因为Hero们就像是从流水线上生产的产品
// 所以你给这个函数取名为HeroFacroty
function HeroFactory(name, title, location) {
  let speciality;
  switch (location) {
    case "射手":
      speciality = "移动的ATM";
      break;
    case "刺客":
      speciality = "AD去质器";
      break;
  }
  return new Hero(name, title, location, speciality);
}
const Caitlyn = HeroFactory("凯特琳", "皮城女警", "射手");
const Zed = HeroFactory("劫", "影流之主", "刺客");

抽象工厂模式

// 写完英雄后,该写游戏模式了
// 首先,总结下游戏模式有哪些属性
function Mode(type, map, mechanism) {
  this.type = type; // 类型
  this.map = map; // 地图
  this.mechanism = mechanism; // 机制
}
// 模仿HeroFactory,写ModeFactory
function ModeFactory(type) {
  let map, mechanism;
  switch (type) {
    case "极地大乱斗":
      map = "...";
      mechanism = "...";
      break;
    case "无限火力":
      map = "...";
      mechanism = "...";
      break;
    case "克隆大作战":
      map = "...";
      mechanism = "...";
      break;
    default:
      map = "...";
      mechanism = "...";
      break;
  }
  return new Mode(type, map, mechanism);
}
const ultraRapidFire = ModeFactory("无限火力");
// 看起来真是岁月静好
// 直到某天,设计师开始整活了
// 他提出给元素龙加一条“吴奇龙”
// 于是你改写了关于召唤师峡谷地图的代码
// 几天后,设计师又让你再加一条“史泰龙”
// 你不得已再一次改写了一次代码
// 数次改写导致ModeFactory愈发混乱、臃肿,难以维护
// 你突发奇想,不如也抽象出关于map和mechanism的工厂
function MapFactory(type) {
  switch (type) {
    case "召唤师峡谷":
    // 处理召唤师峡谷的逻辑代码,其他地图同理
    case "嚎哭深渊":
    case "屠夫之桥":
    case "飞升之地":
  }
}
function MechanismFactory(type) {
  // ...
}
// 如此一来,如果要改动召唤师峡谷
// 就只需要改动MapFactory中对应的那一部分
// 但如果地图种类繁多、逻辑复杂
// MapFactory还是会出现和ModeFactory一样的问题
// 所以我们可以把各个地图再独立出来
// (MechanismFactory同理)
function MapSummonersRift() {
  // 仅存放召唤师峡谷相关的代码
}
function MapHowlingAbyss() {
  // 仅存放嚎哭深渊相关的代码
}
function MapFactory(type) {
  switch (type) {
    case "召唤师峡谷":
      return new MapSummonersRift();
    case "嚎哭深渊":
      return new MapHowlingAbyss();
  }
}
// 如此一来,工厂也有层级之分了
// ModeFactory是管理着MapFactory和MechanismFactory的超级工厂
// MapFactory下面又管理者MapSummonersRift、MapHowlingAbyss等
function ModeFactory(type) {
  let map, mechanism;
  switch (type) {
    case "极地大乱斗":
      map = MapFactory("嚎哭深渊");
      mechanism = MechanismFactory("...");
      break;
    default:
      map = MapFactory("召唤师峡谷");
      mechanism = MechanismFactory("...");
      break;
  }
  return new Mode(type, map, mechanism);
}
// 日子再次回归于岁月静好,直到某天,策划出了个皮尔特沃夫限定活动
// 极地大乱斗的嚎哭深渊需要改成屠夫之桥,但游戏机制不变
// 你不得不回头修改ModeFactory中type的判别逻辑(而且再活动结束后,还得再改回来)
// 这种对“已建工厂”的修改违反了开放封闭原则(类、模块、函数可以扩展,但是不可修改的原则)
// 你思考着,如何避免直接修改工厂内部的逻辑呢?不如试试将Mode像Map那样独立出来
function ModeDefault() {
  this.map = new MapSummonersRift();
  this.mechansims = new MechansismsDefault();
}
function ModePolar() {
  this.map = new MapHowlingAbyss();
  this.mechansims = new MechansismsPolar();
}
const ModeDefaultInstance = new ModeDefault();
const ModePolarInstance = new ModePolar();
// 新模式的添加不会影响已有模式
function ModePiltover() {
  this.map = new MapButchersBridge();
  this.mechansims = new MechansismsPolar();
}
const ModePiltoverInstance = new ModePiltover();
// 然而这样做,产生了一个新的问题
// 对于Mode们有哪些属性、方法,没有成文的规定
// 机智的你从Java里的抽象类中获取了灵感,
// 借用“抽象工厂”和”抽象产品“来规范mode、map、mechansims格式
class ModeFactory {
  createMap() {
    throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
  }
  createMechansims() {
    throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
  }
}
class Map {
  init() {
    throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
  }
}
class Mechansims {
  init() {
    throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
  }
}
// 下面是具体的map(具体的mechansims略)
class MapSummonersRift extends Map {
  init() {
    //
  }
}
class MapHowlingAbyss extends Mechansims {
  init() {
    //
  }
}
// 具体的mode
class ModeDefaultFactory extends ModeFactory {
  createMap() {
    return new MapSummonersRift();
  }
  createMechansims() {
    return new MechansismsDefault();
  }
}
const ModeDefaultInstance = new ModeDefaultFactory();
// 下面我们来总结一下:
// ModeFactory,抽象工厂:声明最终目标产品的共性、规范
// ModeDefaultFactory,具体工厂:继承自抽象工厂,实现了抽象工厂里声明的方法,用于实例化最终目标产品
// Map,Mechansims,抽象产品:具体工厂里实现的方法,会依赖一些产品,抽象产品声明这些产品的共性、规范
// MapSummonersRift,MapHowlingAbyss,具体产品:见抽象产品

单例模式

// 游戏内需要添加一个用于记录对局中数据的对象
// 而且这个对象由十个玩家共用
class GameInfo {
  //
}
// 在玩家1这边:
const gameInfo1 = new GameInfo();
// 在玩家2这边:
const gameInfo2 = new GameInfo();
// 然而玩家1和玩家2并没有共有同一个对象
gameInfo1 === gameInfo2; // false
// 我们为GameInfo添加静态方法
// 以保证多次调用getInstance获取的是同一对象
class GameInfo {
  static getInstance() {
    if (!GameInfo.instance) {
      GameInfo.instance = new GameInfo();
    }
    return GameInfo.instance;
  }
}
const gameInfo1 = GameInfo.instance();
const gameInfo2 = GameInfo.instance();
gameInfo1 === gameInfo2; // true

原型模式

// 你应该很熟悉构造函数、原型对象、实例、原型链等概念了
// 而在JavaScript中,原型编程范式的体现就是基于原型链的继承
// 下面总结一个常见的面试题:实现深拷贝
// 方法一,缺点是没法拷贝函数和正则
function deepClone(obj) {
  let str = JSON.stringify(obj);
  return JSON.parse(str);
}
// 方法二,利用递归,从原理层面实现了深拷贝
function deepClone(obj) {
  // 如果obj是null或基本类型,直接返回
  if (obj === null || typeof obj !== "object") {
    return obj;
  }
  // 初始化复制到的值
  let value;
  if (obj instanceof Array) {
    copy = [];
  } else {
    copy = {};
  }
  // 遍历obj
  for (let key in obj) {
    // 如果key是obj的自有属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用深拷贝方法
      copy[key] = deepClone(obj[key]);
    }
  }
  return copy;
}

结构型

装饰器模式

// 想象这里有一坨屎山,它虽然实现了业务功能,但代码乱七八糟
var falsebbb = "rld",
  aaatrue = "hel",
  shit = "lo wo",
  dsadq2 = aaatrue + shit + falsebbb;
// 我们把屎山的逻辑抽离出来,包装成函数
function oldShit() {
  // 屎山,也就是上面的代码
}
// 老板让我们加一个新功能,我们把新功能写在新函数(装饰器)里
function newShit() {
  // 自己写的新屎山
}
// 把新旧功能合并包装一下
function moreShit() {
  oldShit();
  newShit();
}
// 这种“只添加,不修改”的模式就是装饰器模式,新屎旧屎互不影响

适配器模式

// 你写了个好用的函数
function newAndGood(params) {
  let { sex, name, age } = params;
  // 业务逻辑
}
// 公司有段陈年代码
function oldAndBad(name, sex, age) {
  // 业务逻辑
}
// 老板让你升级一下旧代码,于是你写了个适配器
function OldApatar(params) {
  newAndGood(params);
}
function oldAndBad(name, sex, age) {
  oldApatar({ sex, name, age });
}
// 适配器类似于转换器,譬如typec转接口

代理模式

// 代理,类似于房产中介、fanqiang软件,等等
// ES6中新增了代理器Proxy
const proxy = new Proxy(obj, handler);

// 假设你在b站看片,b站对vip影视库设置了保护代理
const videos = {
  free: ["绝代双骄", "你的名字"],
  vip: ["天气之子", "逃避虽可耻但有用"],
};
let isVIP = false,
  isAdmin = false;
const bilibiliVideos = new Proxy(videos, {
  get(obj, key) {
    if (!isVIP && key === "vip") {
      console.log("不充钱不给看!");
      return;
    } else {
      return obj[key];
    }
  },
  set(obj, key, val) {
    if (!isAdmin) {
      // 这里有个bug,只限制了赋值操作,但仍能通过push等方法修改数组
      // (可以通过添加push代理/拦截器的方法解决这个bug)
      console.log("非管理员无权修改视频库!");
      return;
    } else {
      obj[key] = val;
    }
  },
});

// 事件委托也属于代理模式,父元素代理子元素
const father = document.getElementById("father");
father.addEventListener("click", function (event) {
  event.preventDefault();
  switch (event.target) {
    case "child1":
    //
    case "child2":
    //
  }
});

// 图片预加载亦是,当图片过大时,先给节点设置一个占位图
// 图片资源绑定到不可见节点上,当图片加载完成,再绑定到原先节点
class PreLoadImage {
  // 初始化节点
  constructor(node) {
    this.node = node;
  }
  // 设置src
  setSrc(imageUrl) {
    this.node.src = imageUrl;
  }
}
class ProxyImage {
  // 占位图地址
  static LOADING_URL = "xxxxxx";
  // 初始化代理资源
  constructor(image) {
    this.image = image;
  }
  // 代理设置src
  setSrc(imageUrl) {
    this.image.setSrc(ProxyImage.LOADING_URL);
    // 设置虚拟节点
    const virtualImage = new Image();
    virtualImage.src = imageUrl;
    virtualImage.onload = () => {
      this.image.setSrc(imageUrl);
    };
  }
}
let node = document.getElementById("img"),
  image = new PreloadImage(node),
  proxy = new ProxyImage(image);
proxy.setSrc("图片url");
// 缓存代理
// 求和方法
const sum = (nums) => {
  let length = nums.length,
    result = 0;
  for (let i = 0; i < length; i++) {
    result += nums[i];
  }
  return result;
};

// 为求和方法创建代理
const proxySum = (function () {
  // 求和结果的缓存池
  let cache = {};
  return function () {
    // 将参数字符串作为标识符
    let nums = [...arguments],
      key = nums.join(",");
    return key in cache ? cache[key] : (cache[key] = sum(nums));
  };
})();
// b会直接从缓存池中取值
let a = proxySum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
  b = proxySum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

行为型

策略模式

// 策略模式通过定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
// 换句话说,就是把if-else结构转化为一个或一系列的映射结构
function getData(account) {
  let data;
  switch (account) {
    case "admin":
    // data = 获取管理员相关数据
    case "user":
    // data = 获取用户相关数据
    case "visitor":
    // data = 获取游客相关数据
  }
  return data;
}
// 做一个小转换
const getDataProcessor = {
  admin() {
    // return 获取管理员数据
  },
  user() {
    // return 获取用户相关数据
  },
  visitor() {
    // return 获取游客相关数据
  },
};
function getData(account) {
  return getDataProcessor[account]();
}
// 当然,实际上Processor的映射过程会复杂很多

状态模式

// 状态模式和策略模式相似,但状态模式强调了类的概念
// 即把处理过程放在处理对象本身中
class CoffeeMaker {
  //
  constructor() {
    this.state = "init";
    this.stock = {
      coffee: 500,
      milk: 200,
    };
  }
  //
  changeState(state) {
    this.state = state;
    this.changeStateProcessor[state]();
  }
  //
  changeStateProcessor = {
    that: this,
    coffee() {
      console.log("咖啡机现在的咖啡存储量是:", this.that.stock.coffee);
      console.log("我只吐黑咖啡");
    },
    latte() {
      this.coffee();
      console.log("咖啡机现在的咖啡存储量是:", this.that.stock.milk);
      console.log("加点奶");
    },
    vanillaLatte() {
      this.latte();
      console.log("再加香草糖浆");
    },
    mocha() {
      this.latte();
      console.log("再加巧克力");
    },
  };
}
// 测试一下
const coffeeMaker = new CoffeeMaker();
coffeeMaker.changeState("latte");

观察者模式

// 观察者订阅发布者,发布者能群通知观察者们
class Publisher {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    this.observers.forEach((item, index) => {
      if (item === observer) this.observers.splice(index, 1);
    });
  }
  notifyObersers() {
    this.observers.forEach((item) => {
      item.update(this);
    });
  }
}
class Observer {
  update(publisher) {
    // 更新操作
  }
}
// 在上面两个类中扩展出具体的类
class PMPublisher extends Publisher {
  constructor() {
    super();
    this.state = null;
    this.observers = [];
  }
  getState() {
    return this.state;
  }
  setState(state) {
    this.state = state;
    this.notifyObersers();
  }
}
class PMObserver extends Observer {
  constructor(id) {
    super();
    this.id = id;
    this.state = null;
  }
  update(publisher) {
    this.state = publisher.getState();
    this.work();
  }
  work() {
    console.log(`员工 ${this.id} 接收状态 ${this.state},开始干活啦~`);
  }
}
// 测试一下
const xiaoqiang = new PMPublisher(),
  xiaoting = new PMObserver("#01"),
  xiaoyong = new PMObserver("#02"),
  xiaolong = new PMObserver("#03");
xiaoqiang.addObserver(xiaoting);
xiaoqiang.addObserver(xiaoyong);
xiaoqiang.addObserver(xiaolong);
xiaoqiang.setState("开会");

发布——订阅模式

// 观察者模式中,发布者直接接触订阅者
// 发布——订阅模式中,发布者和订阅者通过中间平台间接接触
// 譬如下面的事件发布/订阅器
class EventEmitter {
  // 构造函数
  constructor() {
    this.handlers = {};
  }

  // 注册事件监听器
  on(event, callback) {
    if (!this.handlers[event]) this.handlers[event] = [];
    this.handlers[event].push(callback);
  }

  // 移除某个事件回调队列里的指定回调函数
  off(event, callback) {
    const callbacks = this.handlers[event],
      index = callbacks.indexOf(callback);
    if (index !== -1) {
      callbacks.splice(index, 1);
    }
  }

  // 触发目标事件
  emit(event, ...params) {
    if (this.handlers[event]) {
      this.handlers[event].forEach((callback) => {
        callback(...params);
      });
    }
  }

  // 为事件注册单次监听器
  once(event, callback) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...params) => {
      callback(...params);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

迭代器模式

// 迭代器模式旨在为不同类型的集合提供遍历接口
// ES6的生成器(迭代器生成函数)能很方便地生成迭代器
function* iteratorGenerator() {
  yield "#1";
  yield "#2";
  yield "#3";
}
const iterator = iteratorGenerator();
iterator.next(); // '#1'
iterator.next(); // '#2'
iterator.next(); // '#3'
// 自己手写一个生成器
function iteratorGenerator(list) {
  let index = 0,
    length = list.length;
  return {
    next() {
      let done = index >= length,
        value = !done ? list[index++] : undefined;
      return { done, value };
    },
  };
}
const iterator = iteratorGenerator(["#1", "#2", "#3"]);
iterator.next(); // '#1'
iterator.next(); // '#2'
iterator.next(); // '#3'
// good!