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.slice
Array.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
方法,这样就不需要理解组件内部复杂的业务逻辑。
不同类型之间再做区分
基于以上的观点,其实我们能知道外观模式和命令模式的区别,就在于命令模式是一份菜单,外部可以自己一个一个点菜,甚至可以撤回点菜。外观模式则是直接点了个全家桶。