JS 设计模式
摘要
- Q:为什么要整理这么一篇文章?
 - A:因为掘金刚好有活动,所以买了本“JavaScript 设计模式核⼼原理与应⽤实践”手册,而且我自己也想看一下 JS 和 JAVA 的设计模式写法的不同之处,也是为了更好地开发。
 
# 开篇
在实际开发中,不发生变化的代码可以说是不存在的。我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定
而这个过程,就叫“封装变化”;这样的代码,就是我们所谓的“健壮”的代码
- 创建型模式封装了创建对象过程中的变化,它做的事情就是将创建对象的过程抽离;
 - 结构型模式封装的是对象之间组合方式的变化,目的在于灵活地表达对象间的配合与依赖关系;
 - 行为型模式则将是对象千变万化的行为进行抽离,确保我们能够更安全、更方便地对行为进行更改。
 
常用的设计模式有 23 种,分类看 这里,这部分的文章只梳理 JS 种常用的几种设计模式
本次涉及到的设计模式
- 创建型
- 工厂模式
 - 抽象工厂模式
 - 单例模式
 - 原型模式
 
 - 结构型
- 装饰器模式
 - 适配器模式
 - 代理模式
 
 - 行为型
- 策略模式
 - 状态模式
 - 观察者模式
 - 发布-订阅模式
 - 迭代器模式 :::
 
 
# 工厂模式和抽象工厂模式
所谓工厂模式,就是将不变的部分进行抽离,将变化的部分进行实例化。举例来说
- 普通工厂模式:
- 手机都有芯片,屏幕,电池这类内部零件,但是手机的品牌又不同,所以不变的是零件,变化的是品牌,这样就能抽离出来了
 
 - 抽象工厂模式:
- 手机都有芯片,屏幕,电池这类内部零件,但是这些零件的品牌又不同,所以不变的是零件,变化的是零件品牌,这样就能再抽离出来,并且可以通过抽象类来规范化
 
 
普通工厂模式
function Mobile({ brand }) {
  this.chip = "芯片";
  this.screen = "屏幕";
  this.cell = "电池";
  this.brand = brand;
}
function MobileFactory(brand = "默认品牌") {
  if (typeof brand !== "string") throw new Error("品牌必选为字符串");
  return new Mobile({ brand });
}
const mi_mobile = new MobileFactory("MI");
const default_mobile = new MobileFactory();
console.log("mi_mobile", mi_mobile);
console.log("default_mobile", default_mobile);
抽象工厂模式
JS 中是没有直接的抽象类的,abstract 是个保留字,但是还没有实现,因此我们需要在类的方法中抛出错误来模拟抽象类,如果继承的子类中没有覆写该方法而调用,就会抛出错误。
/**
 * 芯片类
 */
class Chip {
  getDetail() {
    throw new Error("抽象产品方法不允许直接调用,你需要将我重写!");
  }
}
class XiaoLongCip extends Chip {
  getDetail() {
    return "高通骁龙芯片";
  }
}
class MTKChip extends Chip {
  getDetail() {
    return "联发科芯片";
  }
}
/**
 * 屏幕类
 */
class Screen {
  getDetail() {
    throw new Error("抽象产品方法不允许直接调用,你需要将我重写!");
  }
}
class LGScreen extends Screen {
  getDetail() {
    return "LG屏幕";
  }
}
class KangNingScreen extends Screen {
  getDetail() {
    return "康宁大猩猩屏幕";
  }
}
/**
 * 电池类
 */
class Cell {
  getDetail() {
    throw new Error("抽象产品方法不允许直接调用,你需要将我重写!");
  }
}
class LargeCell extends Cell {
  getDetail() {
    return "5000mA容量电池";
  }
}
class MiddleCell extends Cell {
  getDetail() {
    return "4000mA容量电池";
  }
}
/**
 * 手机基类
 */
function Mobile({ chip, screen, cell, brand }) {
  this.chip = chip;
  this.screen = screen;
  this.cell = cell;
  this.brand = brand;
}
/**
 * 手机品牌工厂类
 * @param { String } brand
 */
function MobileFactory(brand = "默认品牌") {
  if (typeof brand !== "string") throw new Error("品牌必选为字符串");
  let mobileDetail = {
    chip: new MTKChip().getDetail(),
    screen: new LGScreen().getDetail(),
    cell: new MiddleCell().getDetail()
  };
  switch (brand) {
    case "MI":
      mobileDetail = {
        chip: new XiaoLongCip().getDetail(),
        screen: new KangNingScreen().getDetail(),
        cell: new LargeCell().getDetail()
      };
      break;
    default:
      break;
  }
  return new Mobile({ ...mobileDetail, brand });
}
const mi_mobile = new MobileFactory("MI");
const default_mobile = new MobileFactory();
console.log("mi_mobile", mi_mobile);
console.log("default_mobile", default_mobile);
- 封装组件的时候,业务组件最好是创建一个抽象类来进行规范,然后再 extends
 - 组件的要求都是单一封闭原则,只要数据源的输入输出就行
 - 业务组件和可复用组件不同的地方在于,业务组件的处理逻辑包括验证逻辑是写在组件内部的,而可复用组件则是将逻辑抽离出来,让页面去进行处理
 
# 单例模式
所谓单例模式,就是每一次调用都只返回一个实例,常用于提供一个可供全局访问的场景中
ES6-class 版
class SingleModel {
  setItem(key, value) {
    this[key] = value;
  }
  getItem(key) {
    return this[key];
  }
  static getInstance() {
    if (!SingleModel.instance) {
      SingleModel.instance = new SingleModel();
    }
    return SingleModel.instance;
  }
}
const temp1 = SingleModel.getInstance();
const temp2 = SingleModel.getInstance();
temp1.setItem("name", "SingleModel");
temp2.setItem("age", 18);
temp2.getItem("name");
temp1.getItem("age");
闭包版
const SingleModelApply = (function() {
  let instance = null;
  return function() {
    if (!instance) {
      instance = new SingleModel();
    }
    return instance;
  };
})();
function SingleModel() {}
// 这里不要用箭头函数,用箭头函数会指向 window
SingleModel.prototype.setItem = function(key, value) {
  this[key] = value;
};
SingleModel.prototype.getItem = function(key) {
  return this[key];
};
const temp1 = new SingleModelApply();
const temp2 = new SingleModelApply();
temp1.setItem("name", "SingleModel");
temp2.setItem("age", 18);
temp2.getItem("name");
temp1.getItem("age");
# 原型模式
其实 Javascript 的原型模式不用多讲,因为 Javascript 本来就是以原型模式为基础去设计的语言。就算是 ES6 的 class,其本质也是原型继承的语法糖
原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。在 JavaScript 中,原型编程范式的体现就是基于原型链的继承。这其中,对原型、原型链的理解是关键。
关于原型模式和原型继承模式有其他的文章去讨论,可以看下 这里
讲一讲深拷贝,因为深拷贝其实也是原型模式的一种体现。
浅拷贝和深拷贝的区别
浅拷贝出来的对象指向的内存地址是一致的,改 a 的同时会改 b。而深拷贝则是完完全全复制一份,复制出来的对象和元对象完全没有关系。
深拷贝与浅拷贝的概念只存在于引用类型
# 常用的浅拷贝方法
- 对象
Object.assign(target,source)- 可以使用扩展运算符
{...obj}的方式,和Object.assign()行为一致 
 - 数组
Array.prototype.sliceArray.prototype.concat
 
# 常用的深拷贝方法
- 能够实现多层的深拷贝最简单的是 
JSON.parse(JSON.stringify(obj)),但随着对象深度越深越容易耗性能,并且这个方法存在一些局限性,比如会忽略 undefined、symbol,无法处理 function、正则、Date 等等 —— 只有当你的对象是一个严格的 JSON 对象时,可以顺利使用这个方法。 - 第三方库:jquery 的 
$.extend和 lodash 的_.cloneDeep来解决深拷贝 - 当然我们也可以自己简单写一个,简单地处理边界值,使用递归的方式去循环
 
const deepClone = obj => {
  let copyData = {};
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }
  if (Array.isArray(obj)) {
    copyData = [];
  }
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copyData[key] = deepClone(obj[key]);
    }
  }
  return copyData;
};
# 装饰器模式
所谓装饰器模式,只是进行点缀,没有也可以,有的话更好,十分灵活,是比较容易理解的一种模式。
以生活中的例子来说。一杯奶茶,有了珍珠,那就是一杯珍珠奶茶,没有了就是一杯原味奶茶。所以“加珍珠”的操作是可抽离的,这就是装饰器的本意
1、需要扩展一个类的功能。
2、动态的为一个对象增加功能,而且还能动态撤销。(继承不能做到这一点,继承的功能是静态的,不能动态增删。)
- 装饰器模式的优势在于其极强的灵活性和可复用性——它本质上是一个函数,而且往往不依赖于任何逻辑而存在。
 - 在 ES7 中,就有装饰器的概念,它只能用于类和类的方法,但目前需要用 babel 来进行转译,在浏览器和 node 环境暂不支持装饰器语法。
 - babel 转译时需要的插件 
babel-preset-env,babel-plugin-transform-decorators-legacy,嫌本地安装麻烦的话可以上 babel 官网在线转译后再到浏览器调试 - 在 Vue 和 React 中也可以使用装饰器语法,如果用过 
Vue + TSX,加上使用vue-property-decorator,vuex-module-decorators等一系列工具,那开发起来简直和Java的体验差不多 
简单写一个装饰器的使用例子
// 给一个类添加装饰器时,此处的 target 就是被装饰的类 `MilkTea` 本身。
function init(target, key, descriptor) {
  target.description = "这是一杯奶茶!";
  return target;
}
// 注意这里的 `target` ,因为应用到类成员上,所以这时是 `MilkTea.prototype`
// 因为装饰器函数执行的时候,MilkTea 实例还不存在,所以装饰器实际上是作用到 `MilkTea.prototype` 上的
function addPearl(target, key, descriptor) {
  let oldValue = descriptor.value;
  descriptor.value = function() {
    console.log("加了珍珠后,就是珍珠奶茶了!");
    this.title = "珍珠奶茶!";
    return oldValue.call(this, arguments);
  };
  return descriptor;
}
function delPearl(target, key, descriptor) {
  console.log("不管实例里有没有调用到,只要声明了,都会先预加载的");
  let oldValue = descriptor.value;
  descriptor.value = function() {
    this.title = "原味奶茶!";
    return oldValue.call(this, arguments);
  };
  return descriptor;
}
const log = str => (target, key, descriptor) => {
  let oldValue = descriptor.value;
  descriptor.value = function() {
    console.log(str);
    return oldValue.call(this, arguments);
  };
  return descriptor;
};
@init
class MilkTea {
  constructor() {
    this.title = "原味奶茶!";
  }
  @addPearl
  getPearlMilkTea() {
    return this.title;
  }
  // 装饰器加载顺序是从下往上的
  @delPearl
  @log("去掉珍珠了!")
  getOriginTea() {
    return this.title;
  }
}
const milkTea = new MilkTea();
console.log("这是一杯", milkTea.getPearlMilkTea());
console.log("这是一杯", milkTea.getOrigin());
以上编写的例子并不是很准确,但是提供了一种方式去理解 es7 中装饰器的使用方式。
对装饰器有兴趣的话可以看下 core-decorators 的源码
# 适配器模式
适配器模式 Adapter ,某种意义上其实就是转换器,将我自己的数据通过适配器的模式转化成对方想要的数据,或者相反。说白了其实就是兼容模式
一个好的适配器最主要的就是将变化留给自己,将统一留给调用着。有着统一的入参,统一的出参,统一的规则
const getOptions = () => {
  return [
    {
      label: "选项A",
      value: "optionA"
    },
    {
      label: "选项B",
      value: "optionB"
    }
  ];
};
async function Adapter(options) {
  return new Promise((resolve, reject) => {
    if (Array.isArray(options)) {
      const result = new Map();
      options.forEach(opt => {
        result.set(opt.value, opt);
      });
      resolve(result);
    } else {
      reject(false);
    }
  });
}
const options = getOptions();
console.log("options", options);
setTimeout(async () => {
  const optionsMap = await Adapter(options);
  console.log("optionsMap", optionsMap);
});
像很多开源库都是使用适配器来进行处理的,比如 axios 就是用适配器来兼容 Node 环境和 Browser 环境
# 代理模式
代理,这个词汇其实很熟悉的,日常生活中,朋友圈总要很多产品代理人,然后推荐这个推荐那个。如果在国内想当个跟进时代的程序员,通过代理进行科学上网也是必不可少的,所以其实大家都很熟悉这个词。
对 JS 来说,ES6 的 Proxy 可能让你更加熟悉,毕竟这个新特性一出,Vue 都可以依此升级一个大版本了。先来看示例
const obj = {
  name: "H-zk",
  position: "font-end Engineer",
  sex: "man"
};
const handler = {
  get(target, propKey, proxy) {
    if (propKey === "sex") {
      return "unknow";
    } else {
      return target[propKey];
    }
  },
  set(target, propKey, value) {
    const keys = Object.keys(target);
    if (keys.includes(propKey)) {
      throw new Error("已存在该属性,不能覆盖");
    } else {
      target[propKey] = value;
    }
  }
};
const proxy = new Proxy(obj, handler);
console.log("name", proxy.name);
console.log("sex", proxy.sex);
proxy.age = 18;
console.log("age", proxy.age);
proxy.sex = "unknow";
从示例可以看出,其实 JS 中的 proxy 就是用来控制对某个对象的访问和赋值,相当于这个对象的过滤器,本身就是为拦截而生的,常用于保护代理。
目前 proxy 的兼容性并不好,IE 基本就没戏了,所以慎用,要使用的话需要找对应的 polyfill 和性能调研
当然,开发中使用 Proxy 常用于保护代理,JS 中还有其他的代理,比如说,事件代理,虚拟代理,缓存代理等。
举例
- 事件代理 - 常用的是通过事件冒泡机制,在父级去获取对应的节点触发对应的事件
 - 虚拟代理 - 图片预加载占位,然后通过新建 new Image 对象,提前获取数据,之后用户访问即可从浏览器缓存中读取
 - 缓存代理 - 在 proxy 的 get 方式中设置缓存池,可以将如果入参和调用方法一致,就可以直接从缓存池中读取
 
# 策略模式
终于来到最常用的“策略模式”了,为什么说最常用呢,因为它很容易实现,而且很实用,如果你有过一段时间的业务开发经验,就会知道它有多么实用。
先来看这冗长的 if else 示例
function getData(option, numA, numB) {
  if (option === "add") {
    return numA + numB;
  } else if (option === "substract") {
    return numA - numB;
  } else if (option === "multipy") {
    return numA * numB;
  } else if (option === "divide") {
    return numA / numB;
  } else {
    return 0;
  }
}
作为一个进入编程熟手的程序员,你肯定被这一堆类似的 if else 的条件语句给逼急过,如果要再加上新功能,就只能再加一个判断,后续维护的人还要再看整个 getData 函数才能保证开发,很不稳定。所以就需要去使用策略模式,策略模式就是解决这冗长的 if else 最佳手段。
使用策略模式重构过的代码
const optionMap = {
  add(numA, numB) {
    return numA + numB;
  },
  substract(numA, numB) {
    return numA - numB;
  },
  multipy(numA, numB) {
    return numA * numB;
  },
  divide(numA, numB) {
    return numA / numB;
  }
};
function getData(option, numA, numB) {
  return option in optionMap ? optionMap[option](numA, numB) : 0;
}
使用了策略模式前,对于需求的变更开发人员只能更改 getData 函数,使用策略模式后,由于使用了 optionMap 来作为映射/配置项,开发人员直接再新增一个函数就可以了,不会影响之前的逻辑,后续维护也方便。
将对应的逻辑提取,封装到一个映射对象中,再通过关键标识符来进行分发优化,这就是策略模式的套路。
# 状态模式
状态模式和策略模式非常像,不同的是,策略模式中的行为函数只关注入参和出参,说白了,就是一段独立的算法逻辑的封装,并且每个行为函数都互相平行,各不相关。写策略模式时,只需要关注内部的算法逻辑就行。而状态模式就不那么独立了,
状态模式还需要关注对应的调用主体,状态模式中的行为函数和调用主体之间存在着关联。这里以之前提过的“奶茶”作为示例,并通过状态模式来进行重构。
class MilkTea {
  constructor() {
    this.milk = 500;
    this.tea = 50;
    this.peal = 500;
    this.lemon = 50;
  }
  validateMaterials() {
    if (this.milk <= 0) {
      throw new Error("牛奶不够了");
    }
    if (this.tea <= 0) {
      throw new Error("茶叶不够了");
    }
    if (this.peal <= 0) {
      throw new Error("珍珠不够了");
    }
    if (this.lemon <= 0) {
      throw new Error("柠檬不够了");
    }
  }
  getCategory(category) {
    this.validateMaterials();
    if (category === "general") {
      this.milk -= 100;
      this.tea -= 5;
      console.log("得到一杯原味奶茶!");
    } else if (category === "pealMilkTea") {
      this.milk -= 100;
      this.tea -= 5;
      this.peal -= 20;
      console.log("得到一杯珍珠奶茶!");
    } else if (category === "lemonMilkTea") {
      this.milk -= 100;
      this.tea -= 5;
      this.lemon -= 2;
      console.log("得到一杯柠檬奶茶!");
    } else {
      throw new Error("暂时不提供其他种类的奶茶哦!");
    }
  }
}
const milkTea = new MilkTea();
milkTea.getCategory("general");
milkTea.getCategory("other");
根据上面的代码要如何重构好呢?仔细看 getCategory 函数,每一次都要检验所有的原材料,而且获得一种新类型的奶茶,都要手动去减少牛奶和茶叶的数量,这也太麻烦和耦合了吧。那根据业务逻辑来说,一杯奶茶,就是由无数配料构建成的,如果把配料封装起来,然后再去调用这个方法呢?是的,接下来的示例就是用状态模式来重构了
class MilkTea {
  constructor() {
    this.milk = 500;
    this.tea = 50;
    this.peal = 500;
    this.lemon = 50;
  }
  getMilk() {
    if (this.milk <= 0) {
      throw new Error("牛奶不够了");
    }
    this.milk -= 100;
  }
  getTea() {
    if (this.tea <= 0) {
      throw new Error("茶叶不够了");
    }
    this.tea -= 5;
  }
  getPeal() {
    if (this.peal <= 0) {
      throw new Error("珍珠不够了");
    }
    this.peal -= 20;
  }
  getLemon() {
    if (this.lemon <= 0) {
      throw new Error("柠檬不够了");
    }
    this.lemon -= 2;
  }
  categoryMap = {
    origin: this,
    general() {
      this.origin.getMilk();
      this.origin.getTea();
      console.log("得到一杯原味奶茶!");
    },
    pealMilkTea() {
      this.general();
      this.origin.getPeal();
      console.log("再加一点珍珠,就变成一杯珍珠奶茶!");
    },
    lemonMilkTea() {
      this.general();
      this.origin.getLemon();
      console.log("再加一点柠檬,就变成一杯柠檬奶茶!");
    }
  };
  getCategory(category) {
    if (category in this.categoryMap) {
      return this.categoryMap[category]();
    } else {
      throw new Error("暂时不提供其他种类的奶茶哦!");
    }
  }
}
const milkTea = new MilkTea();
milkTea.getCategory("general");
milkTea.getCategory("other");
对比两个版本,会发现使用状态模式后,整个 MilkTea 的逻辑更加清楚,后续的需求变更和维护也很容易再处理,而且并不用去控制具体的操作,只要调用对应的函数即可。
# 观察者模式
观察者模式,是所有 JavaScript 设计模式中使用频率最高,面试频率也最高的设计模式,所以说它十分重要
Vue 的双向绑定的实现原理其实就是观察者模式的实现
在 Vue 中,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式。
Vue2.x 是基于 Object.defineProperty 来实现观察者模式的,所以这里也以此为示例
/**
 * 观察用的函数
 * @param { Object } target 被观察的对象
 * @param { Dep } dep  观察者
 */
function observer(target, dep) {
  if (target && typeof target === "object") {
    Object.keys(target).forEach(key => {
      defineReactive(target, key, target[key], dep);
    });
  }
}
function defineReactive(target, key, val, dep) {
  observer(val, dep);
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: false,
    get: () => val,
    set: value => {
      dep.notify(`${key}改变,${val}=>${value}`);
      val = value;
    }
  });
}
// 定义观察者类Dep
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify(msg) {
    this.subs.forEach(sub => {
      sub.update(msg);
    });
  }
}
// 定义订阅者类Dep
class Subscibe {
  constructor(name) {
    this.name = name;
  }
  update(msg) {
    console.log(`通知${this.name},数据更新了,${msg}`);
  }
}
// 被订阅的对象
const example = {
  foo: "bar",
  name: "example",
  obj: {
    key: "test"
  }
};
const dep = new Dep();
const sub1 = new Subscibe("sub1");
const sub2 = new Subscibe("sub2");
const sub3 = new Subscibe("sub3");
dep.addSub(sub1);
dep.addSub(sub2);
dep.addSub(sub3);
// 开始进行监听观察
observer(example, dep);
// 当被订阅的对象进行变动时,观察者会自动通知给订阅者
example.foo = "foo";
example.name = "test";
example.obj.key = "test-key";
defineReactive(example, "bar", "foo", dep);
example.bar = "bar";
注意
观察者模式 和 发布-订阅模式 相似,但并不同
- 发布者直接触及到订阅者的操作,叫观察者模式
 - 发布者不直接触及到订阅者、而是由统一的第三方(事件中心)来完成实际的通信的操作,叫做发布-订阅模式。
 
所以重写下“观察者模式”的例子如下
// 定义事件中心类 EventCenter
class EventCenter {
  constructor() {
    this.subs = [];
    this.publicher = null;
    this.data = {
      foo: "bar",
      name: "example",
      obj: {
        key: "test"
      }
    };
    this.observer(this.data);
  }
  addPublicher(publicher) {
    this.publicher = publicher;
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  changeData(key, value) {
    if (value && typeof value === "object") {
      Object.keys(value).forEach(i => {
        this.data[key][i] = value[i];
      });
    } else {
      this.data[key] = value;
    }
  }
  notify(msg) {
    this.subs.forEach(sub => {
      sub.update(msg);
    });
  }
  /**
   * 观察用的函数
   * @param { Object } target 被观察的对象
   */
  observer(target) {
    if (target && typeof target === "object") {
      Object.keys(target).forEach(key => {
        this.defineReactive(target, key, target[key]);
      });
    }
  }
  defineReactive(target, key, val) {
    this.observer(val);
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: false,
      get: () => val,
      set: value => {
        this.notify(`${key}改变,${val}=>${value}`);
        val = value;
      }
    });
  }
}
// 定义发布者类 Publich
class Publich {
  constructor({ name, eventCenter }) {
    this.name = name;
    this.eventCenter = eventCenter;
  }
  changeData(target) {
    if (!this.eventCenter) throw new Error("没有关联事件中心");
    if (target && typeof target === "object") {
      Object.keys(target).forEach(key => {
        this.eventCenter.changeData(key, target[key]);
      });
    }
  }
}
// 定义订阅者类publicher
class Subscibe {
  constructor(name) {
    this.name = name;
  }
  update(msg) {
    console.log(`通知${this.name},数据更新了,${msg}`);
  }
}
// 初始化
const eventCenter = new EventCenter();
const pub1 = new Publich({
  name: "pub1",
  eventCenter
});
const sub1 = new Subscibe("sub1");
const sub2 = new Subscibe("sub2");
const sub3 = new Subscibe("sub3");
eventCenter.addPublicher(pub1);
eventCenter.addSub(sub1);
eventCenter.addSub(sub2);
eventCenter.addSub(sub3);
pub1.changeData({ foo: "foo", name: "test", obj: { key: "test-key" } });
# 迭代器模式
迭代器模式是设计模式中少有的目的性极强的模式。所谓“目的性极强”就是说它不操心别的,它就解决这一个问题——遍历.
在开发中常用的迭代器有这几种
- 常用的 for 语句和对象的 for in 语句
 - ES5 中 Array.prototype.forEach 的方法来遍历数据
 - jQuery 里中的 $.each 函数
 - ES6 在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator)。
 - ES6 中的 Generator 函数
 
在 ES6 中,针对 Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。
ES6 约定,任何数据结构只要具备 Symbol.iterator 属性(这个属性就是 Iterator 的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被 for...of...循环和迭代器的 next 方法遍历。 事实上,for...of...的背后正是对 next 方法的反复调用。
const arr = [1, 2, 3];
for (let item of arr) {
  console.log(`当前元素是${item}`);
}
// ========================
const iterator = arr[Symbol.iterator]();
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
使用 Generator 编写一个迭代器生成函数
function* iteratorGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
const iterator = iteratorGenerator();
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
最后自己实现一个类似的迭代器
function iteratorGenerator(list) {
  let idx = 0;
  let len = list.length;
  return {
    next: function() {
      let done = idx >= len;
      let value = !done ? list[idx++] : undefined;
      return {
        value,
        done
      };
    }
  };
}
const iterator = iteratorGenerator([1, 2, 3]);
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
console.log(`当前是${JSON.stringify(iterator.next())}`);
# 结语
实际上,最后这份文章,不,应该说是笔记也只是整理了日常开发可能会用到的设计模式,但是能够在正确的使用场景下用到这些模式,在我个人看来就是一个很不错的代码编写能力提升了,希望能够写出优雅健壮的代码
最后,来看一下最重要的部分,因为每次学完设计模式,我都会把它们搞混,所以把会搞混的模式整理了下来并进行对比
# 模式对比!代理模式 - 外观模式 - 装饰器模式 - 中介者模式 - 命令模式
首先确定类型
- 代理模式,外观模式,装饰器模式属于结构型。
 - 中介者模式,命令模式属于行为型
 
结构型模式在于对原有的对象进行加强,是在原有的基础上再加以去控制/过滤/转换。
而行为型不同,行为型最主要是用来解耦,将复杂的抽离,变成一个第三方的独立对象,然后去跟各个对象进行交互,并且交互的只是数据,不影响被交互的对象本身的状态。
所以关键点在于作用的主体不同。结构型在于被调用的对象本身,行为型则在于一个新的第三方对象
在同一类型内再进行区分
代理模式和装饰器模式很好区别,使用装饰器模式,你访问还是原来的对象, 使用代理模式,访问的则是被代理过的对象,注意,这里被代理过的对象,并不是一个第三方独立对象,因为修改被代理过的对象,同样会影响到原来的对象。
外观模式则与代理模式和装饰器模式不同,外观模式某种意义上是为高层提供一个简化的操作,隐藏底层的复杂性。比方我们建立多个业务组件,然后每个组件都有一个
getData的方式去获取组件内部的数据,并且这个方式会按照特定的格式返回。外观模式只需要提供getAllData这个方法和最后得到数据,然后由getAllData去调用组件提供的getData方法,这样就不需要理解组件内部复杂的业务逻辑。
不同类型之间再做区分
基于以上的观点,其实我们能知道外观模式和命令模式的区别,就在于命令模式是一份菜单,外部可以自己一个一个点菜,甚至可以撤回点菜。外观模式则是直接点了个全家桶。