# 创建型模式: 创建对象的模式,抽象了实例化的过程
# 单例模式
# 什么是单例模式?
单例模式定义:保证一个类仅有一个实例,并提供访问此实例的全局访问点。
# 单例模式用途
如果一个类负责连接数据库的线程池、日志记录逻辑等等,此时需要单例模式来保证对象不被重复创建,以达到降低开销的目的。
const Singleton = function() {};
Singleton.getInstance = (function() {
// 由于es6没有静态类型,故闭包: 函数外部无法访问 instance
let instance = null;
return function() {
// 检查是否存在实例
if (!instance) {
instance = new Singleton();
}
return instance;
};
})();
let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2);
# 工厂模式
# 什么是工厂模式?
工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。”
简单来说:就是把new对象的操作包裹一层,对外提供一个可以根据不同参数创建不同对象的函数。
# 工厂模式的优缺点
优点显而易见,可以隐藏原始类,方便之后的代码迁移。调用者只需要记住类的代名词即可。
由于多了层封装,会造成类的数目过多,系统复杂度增加。
# ES6 实现
调用者通过向工厂类传递参数,来获取对应的实体。在这个过程中,具体实体类的创建过程,由工厂类全权负责。
/**
* 实体类:Dog、Cat
*/
class Dog {
run() {
console.log("狗");
}
}
class Cat {
run() {
console.log("猫");
}
}
/**
* 工厂类:Animal
*/
class Animal {
constructor(name) {
name = name.toLocaleLowerCase();
switch (name) {
case "dog":
return new Dog();
case "cat":
return new Cat();
default:
throw TypeError("class name wrong");
}
}
}
/**
* 以下是测试代码
*/
const cat = new Animal("cat");
cat.run();
const dog = new Animal("dog");
dog.run();
# 抽象工厂模式
抽象工厂模式就是:围绕一个超级工厂类,创建其他工厂类;再围绕工厂类,创建实体类。
相较于传统的工厂模式,它多出了一个超级工厂类。
# 什么是抽象工厂模式?
抽象工厂模式就是:围绕一个超级工厂类,创建其他工厂类;再围绕工厂类,创建实体类。
相较于传统的工厂模式,它多出了一个超级工厂类。
它的优缺点与工厂模式类似,这里不再冗赘它的优缺点,下面直接谈一下实现吧。
# 准备实体类
按照之前的做法,这里我们实现几个实体类:Cat 和 Dog 一组、Male 和 Female 一组。
// 实体类
//按照之前的做法,这里我们实现几个实体类:Cat 和 Dog 一组、Male 和 Female 一组。
class Dog {
run() {
console.log("狗");
}
}
class Cat {
run() {
console.log("猫");
}
}
/*************************************************/
class Male {
run() {
console.log("男性");
}
}
class Female {
run() {
console.log("女性");
}
}
// 工厂类
// 假设 Cat 和 Dog,属于 Animal 工厂的产品;Male 和 Female 属于 Person 工厂的产品。所以需要实现 2 个工厂类:Animal 和 Person。
// 由于工厂类上面还有个超级工厂,为了方便工厂类生产实体,工厂类应该提供生产实体的方法接口。
为了更好的约束工厂类的实现,先实现一个抽象工厂类:
class AbstractFactory {
getPerson() {
throw new Error("子类请实现接口");
}
getAnimal() {
throw new Error("子类请实现接口");
}
}
# 接下来,Animal 和 Dog 实现抽象工厂类(AbstractFactory):
class PersonFactory extends AbstractFactory {
getPerson(person) {
person = person.toLocaleLowerCase();
switch (person) {
case "male":
return new Male();
case "female":
return new Female();
default:
break;
}
}
getAnimal() {
return null;
}
}
class AnimalFactory extends AbstractFactory {
getPerson() {
return null;
}
getAnimal(animal) {
animal = animal.toLocaleLowerCase();
switch (animal) {
case "cat":
return new Cat();
case "dog":
return new Dog();
default:
break;
}
}
}
# 实现“超级工厂”
超级工厂的实现没什么困难,如下所示:
class Factory {
constructor(choice) {
choice = choice.toLocaleLowerCase();
switch (choice) {
case "person":
return new PersonFactory();
case "animal":
return new AnimalFactory();
default:
break;
}
}
}
看看怎么使用超级工厂 实现了那么多,还是要看用例才能更好理解“超级工厂”的用法和设计理念:
/**
* 以下是测试代码
*/
// 创建person工厂
const personFactory = new Factory("person");
// 从person工厂中创建 male 和 female 实体
const male = personFactory.getPerson("male"),
female = personFactory.getPerson("female");
// 输出测试
male.run();
female.run();
// 创建animal工厂
const animalFactory = new Factory("animal");
// 从animal工厂中创建 dog 和 cat 实体
const dog = animalFactory.getAnimal("dog"),
cat = animalFactory.getAnimal("cat");
// 输出测试
dog.run();
cat.run();
# 结构型模式:
解决怎样组装现有对象,设计交互方式,从而达到实现一定的功能目的。例如,以封装为目的的适配器和桥接,以扩展性为目的的代理、装饰器
# 享元模式
享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。
# 什么是“享元模式”?
享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。
- 享元模式提醒我们将一个对象的属性划分为内部和外部状态。
- 内部状态:可以被对象集合共享,通常不会改变
- 外部状态:根据应用场景经常改变
- 享元模式是利用时间换取空间的优化模式。
# 应用场景
享元模式虽然名字听起来比较高深,但是实际使用非常容易:只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。
为了显示它的强大,下面的代码是简单地实现了大家耳熟能详的“对象池”,以彰显这种设计模式的魅力。
# 代码实现
通过阅读下方代码可以发现:对于File类,内部状态是pool属性和download方法;外部状态是name和src(文件名和文件链接)。借助对象池,实现了File类的复用
// 对象池
class ObjectPool {
constructor() {
this._pool = []; //
}
// 创建对象
create(Obj) {
return this._pool.length === 0
? new Obj(this) // 对象池中没有空闲对象,则创建一个新的对象
: this._pool.shift(); // 对象池中有空闲对象,直接取出,无需再次创建
}
// 对象回收
recover(obj) {
return this._pool.push(obj);
}
// 对象池大小
size() {
return this._pool.length;
}
}
// 模拟文件对象
class File {
constructor(pool) {
this.pool = pool;
}
// 模拟下载操作
download() {
console.log(`+ 从 ${this.src} 开始下载 ${this.name}`);
setTimeout(() => {
console.log(`- ${this.name} 下载完毕`); // 下载完毕后, 将对象重新放入对象池
this.pool.recover(this);
}, 100);
}
}
/****************** 以下是测试函数 **********************/
let objPool = new ObjectPool();
let file1 = objPool.create(File);
file1.name = "文件1";
file1.src = "https://download1.com";
file1.download();
let file2 = objPool.create(File);
file2.name = "文件2";
file2.src = "https://download2.com";
file2.download();
setTimeout(() => {
let file3 = objPool.create(File);
file3.name = "文件3";
file3.src = "https://download3.com";
file3.download();
}, 200);
setTimeout(
() =>
console.log(
`${"*".repeat(
50
)}\n下载了3个文件,但其实只创建了${objPool.size()}个对象`
),
1000
);
/** 下载结果
+ 从 https://download1.com 开始下载 文件1
+ 从 https://download2.com 开始下载 文件2
- 文件1 下载完毕
- 文件2 下载完毕
+ 从 https://download3.com 开始下载 文件3
- 文件3 下载完毕
**************************************************
下载了3个文件,但其实只创建了2个对象
**/
# 代理模式
代理模式的定义:为一个对象提供一种代理以方便对它的访问。
# 什么是代理模式?
代理模式的定义:为一个对象提供一种代理以方便对它的访问。
代理模式可以解决避免对一些对象的直接访问,以此为基础,常见的有保护代理和虚拟代理。保护代理可以在代理中直接拒绝对对象的访问;虚拟代理可以延迟访问到真正需要的时候,以节省程序开销。
# 代理模式优缺点
代理模式有高度解耦、对象保护、易修改等优点。
同样地,因为是通过“代理”访问对象,因此开销会更大,时间也会更慢。
// main.js
const myImg = {
setSrc(imgNode, src) {
imgNode.src = src;
}
};
// 利用代理模式实现图片懒加载
const proxyImg = {
setSrc(imgNode, src) {
myImg.setSrc(imgNode, "./image.png"); // NO1. 加载占位图片并且将图片放入元素
let img = new Image();
img.onload = () => {
myImg.setSrc(imgNode, src); // NO3. 完成加载后, 更新 元素中的图片
};
img.src = src; // NO2. 加载真正需要的图片
}
};
let imgNode = document.createElement("img"),
imgSrc =
"https://upload-images.jianshu.io/upload_images/5486602-5cab95ba00b272bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp";
document.body.appendChild(imgNode);
proxyImg.setSrc(imgNode, imgSrc);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./main.js"></script>
</body>
</html>
# 桥接模式
桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。
# 什么是桥接模式
桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。
在这种模式的实现上,需要一个对象担任“桥”的角色,起到连接的作用。
# 应用场景
在封装开源库的组件时候,经常会用到这种设计模式。
例如,对外提供暴露一个afterFinish函数, 如果用户有传入此函数, 那么就会在某一段代码逻辑中调用。
这个过程中,组件起到了“桥”的作用,而具体实现是用户自定义。
# ES6 实现
JavaScript 中桥接模式的典型应用是:Array对象上的forEach函数。
此函数负责循环遍历数组每个元素,是抽象部分; 而回调函数callback就是具体实现部分。
下方是模拟forEach方法:
const forEach = (arr, callback) => {
if (!Array.isArray(arr)) return;
const length = arr.length;
for (let i = 0; i < length; ++i) {
callback(arr[i], i);
}
};
// 以下是测试代码
let arr = ["a", "b"];
forEach(arr, (el, index) => console.log("元素是", el, "位于", index));
# 装饰者模式
装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。
# 什么是“装饰者模式”?
装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。
根据描述,装饰者显然比继承等方式更灵活,而且不污染原来的代码,代码逻辑松耦合。
# 应用场景
装饰者模式由于松耦合,多用于一开始不确定对象的功能、或者对象功能经常变动的时候。
尤其是在参数检查、参数拦截等场景。
# 代码实现
ES6 的装饰器语法规范只是在“提案阶段”,而且不能装饰普通函数或者箭头函数。
下面的代码,addDecorator可以为指定函数增加装饰器。
其中,装饰器的触发可以在函数运行之前,也可以在函数运行之后。
注意:装饰器需要保存函数的运行结果,并且返回。
const isFn = fn => typeof fn === "function";
const addDecorator = (fn, before, after) => {
if (!isFn(fn)) {
return () => {};
}
return (...args) => {
let result;
// 按照顺序执行“装饰函数”
isFn(before) && before(...args);
// 保存返回函数结果
isFn(fn) && (result = fn(...args));
isFn(after) && after(...args);
// 最后返回结果
return result;
};
};
/******************以下是测试代码******************/
const beforeHello = (...args) => {
console.log(`Before Hello, args are ${args}`);
};
const hello = (name = "user") => {
console.log(`Hello, ${name}`);
return name;
};
const afterHello = (...args) => {
console.log(`After Hello, args are ${args}`);
};
const wrappedHello = addDecorator(hello, beforeHello, afterHello);
let result = wrappedHello("godbmw.com");
console.log(result);
# 组合模式
组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。
# 什么是“组合模式”?
组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。
用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成
单个对象和组合对象对于用户暴露的接口具有一致性,而同种接口不同表现形式亦体现了多态性
# 应用场景
组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。
# 代码实现
这里用代码模拟文件扫描功能,封装了File和Folder两个类。在组合模式下,用户可以向Folder类嵌套File或者Folder来模拟真实的“文件目录”的树结构。
同时,两个类都对外提供了scan接口,File下的scan是扫描文件,Folder下的scan是调用子文件夹和子文件的scan方法。整个过程采用的是深度优先。
// 文件类
class File {
constructor(name) {
this.name = name || "File";
}
add() {
throw new Error("文件夹下面不能添加文件");
}
scan() {
console.log("扫描文件: " + this.name);
}
}
// 文件夹类
class Folder {
constructor(name) {
this.name = name || "Folder";
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
console.log("扫描文件夹: " + this.name);
for (let file of this.files) {
file.scan();
}
}
}
let home = new Folder("用户根目录");
let folder1 = new Folder("第一个文件夹"),
folder2 = new Folder("第二个文件夹");
let file1 = new File("1号文件"),
file2 = new File("2号文件"),
file3 = new File("3号文件");
// 将文件添加到对应文件夹中
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
// 将文件夹添加到更高级的目录文件夹中
home.add(folder1);
home.add(folder2);
// 扫描目录文件夹
home.scan();
# 适配器模式
适配器模式:为多个不兼容接口之间提供“转化器”。
# 什么是适配器模式?
适配器模式:为多个不兼容接口之间提供“转化器”。
它的实现非常简单,检查接口的数据,进行过滤、重组等操作,使另一接口可以使用数据即可。
# 应用场景
当数据不符合使用规则,就可以借助此种模式进行格式转化。
# 多语言实现
假设编写了不同平台的音乐爬虫,破解音乐数据。而对外向用户暴露的数据应该是具有一致性。
下面,adapter函数的作用就是转化数据格式。
事实上,在我开发的音乐爬虫库–music-api-next就采用了下面的处理方法。
因为,网易、QQ、虾米等平台的音乐数据不同,需要处理成一致的数据返回给用户,方便用户调用。
ES6 实现
const API = {
qq: () => ({
n: "菊花台",
a: "周杰伦",
f: 1
}),
netease: () => ({
name: "菊花台",
author: "周杰伦",
f: false
})
};
const adapter = (info = {}) => ({
name: info.name || info.n,
author: info.author || info.a,
free: !!info.f
});
/*************测试函数***************/
console.log(adapter(API.qq()));
console.log(adapter(API.netease()));
# 行为型模式:
描述多个类或对象怎样交互以及怎样分配职责
# 命令模式
命令模式定义:将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
# 什么是“命令模式”?
命令模式(别名:动作模式、事务模式)定义:将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
简单来说,它的核心思想是:不直接调用类的内部方法,而是通过给“指令函数”传递参数,由“指令函数”来调用类的内部方法。
在这过程中,分别有 3 个不同的主体:调用者、传递者和执行者。
# 应用场景
当想降低调用者与执行者(类的内部方法)之间的耦合度时,可以使用此种设计模式。比如:设计一个命令队列,将命令调用记入日志。
// 为了方便演示,mock的假数据
const mockData = {
10001: {
name: "电视",
price: 3888
},
10002: {
name: "MacPro",
price: 17000
}
};
/**
* 商品类(执行者)
*/
class Mall {
static request(id) {
if (!mockData[id]) {
return `商品不存在`;
}
const { name, price } = mockData[id];
return `商品名: ${name} 单价: ${price}`;
}
static buy(id, number) {
if (!mockData[id]) {
return `商品不存在`;
}
if (number < 1) {
return `至少购买1个商品`;
}
return mockData[id].price * number;
}
}
# 备忘录模式
备忘录模式:属于行为模式,保存某个状态,并且在需要的时候直接获取,而不是重复计算。
# 什么是备忘录模式
备忘录模式:属于行为模式,保存某个状态,并且在需要的时候直接获取,而不是重复计算。
注意:备忘录模式实现,不能破坏原始封装。也就是说,能拿到内部状态,将其保存在外部。
# 应用场景
最典型的例子是“斐波那契数列”递归实现。 不借助备忘录模式,数据一大,就容易爆栈;借助备忘录,算法的时间复杂度可以降低到O(N) 除此之外,数据的缓存等也是常见应用场景。
# ES6 实现
首先模拟了一下简单的拉取分页数据。 如果当前数据没有被缓存,那么就模拟异步请求,并将结果放入缓存中; 如果已经缓存过,那么立即取出即可,无需多次请求。
const fetchData = (() => {
// 备忘录 / 缓存
const cache = {};
return page =>
new Promise(resolve => {
// 如果页面数据已经被缓存, 直接取出
if (page in cache) {
return resolve(cache[page]);
}
// 否则, 异步请求页面数据
// 此处, 仅仅是模拟异步请求
setTimeout(() => {
cache[page] = `内容是${page}`;
resolve(cache[page]);
}, 1000);
});
})();
// 以下是测试代码
const run = async () => {
let start = new Date().getTime(),
now;
// 第一次: 没有缓存
await fetchData(1);
now = new Date().getTime();
console.log(`没有缓存, 耗时${now - start}ms`);
// 第二次: 有缓存 / 备忘录有记录
start = now;
await fetchData(1);
now = new Date().getTime();
console.log(`有缓存, 耗时${now - start}ms`);
};
run();
# 模板模式
模板模式是:抽象父类定义了子类需要重写的相关方法。并且这些方法,仍然是通过父类方法调用的。
# 什么是模板模式?
模板模式是:抽象父类定义了子类需要重写的相关方法。并且这些方法,仍然是通过父类方法调用的。
根据描述,父类提供了“模板”并决定是否调用,子类进行具体实现。
# 应用场景
一些系统的架构或者算法骨架,由“BOSS”编写抽象方法,具体的实现,交给“小弟们”实现。
而用不用“小弟们”的方法,还是看“BOSS”的心情。
# ES6 实现
Animal是抽象类,Dog和Cat分别具体实现了eat()和sleep()方法。
Dog或Cat实例可以通过live()方法调用eat()和sleep()。
注意:Cat和Dog实例会被自动添加live()方法。不暴露live()是为了防止live()被子类重写,保证父类的控制权。
class Animal {
constructor() {
// this 指向实例
this.live = () => {
this.eat();
this.sleep();
};
}
eat() {
throw new Error("模板类方法必须被重写");
}
sleep() {
throw new Error("模板类方法必须被重写");
}
}
class Dog extends Animal {
constructor(...args) {
super(...args);
}
eat() {
console.log("狗吃粮");
}
sleep() {
console.log("狗睡觉");
}
}
class Cat extends Animal {
constructor(...args) {
super(...args);
}
eat() {
console.log("猫吃粮");
}
sleep() {
console.log("猫睡觉");
}
}
/********* 以下为测试代码 ********/
// 此时, Animal中的this指向dog
let dog = new Dog();
dog.live();
// 此时, Animal中的this指向cat
let cat = new Cat();
cat.live();
# 状态模式
状态模式:对象行为是根据状态改变,而改变的。
# 什么是“状态模式”?
状态模式:对象行为是根据状态改变,而改变的。
正是由于内部状态的变化,导致对外的行为发生了变化。例如:相同的方法在不同时刻被调用,行为可能会有差。
# 优缺点 优点:
封装了转化规则,对于大量分支语句,可以考虑使用状态类进一步封装。
每个状态都是确定的,对象行为是可控的。
缺点: 状态模式的实现关键是将事物的状态都封装成单独的类,这个类的各种方法就是“此种状态对应的表现行为”。因此,程序开销会增大。
# ES6 实现
在 JavaScript 中,可以直接用 JSON 对象来代替状态类。
下面代码展示的就是 FSM(有限状态机)里面有 3
种状态:download、pause、deleted。控制状态转化的代码也在其中。
DownLoad类就是,常说的Context对象,它的行为会随着状态的改变而改变。
const FSM = (() => {
let currenState = "download";
return {
download: {
click: () => {
console.log("暂停下载");
currenState = "pause";
},
del: () => {
console.log("先暂停, 再删除");
}
},
pause: {
click: () => {
console.log("继续下载");
currenState = "download";
},
del: () => {
console.log("删除任务");
currenState = "deleted";
}
},
deleted: {
click: () => {
console.log("任务已删除, 请重新开始");
},
del: () => {
console.log("任务已删除");
}
},
getState: () => currenState
};
})();
class Download {
constructor(fsm) {
this.fsm = fsm;
}
handleClick() {
const { fsm } = this;
fsm[fsm.getState()].click();
}
hanldeDel() {
const { fsm } = this;
fsm[fsm.getState()].del();
}
}
// 开始下载
let download = new Download(FSM);
download.handleClick(); // 暂停下载
download.handleClick(); // 继续下载
download.hanldeDel(); // 下载中,无法执行删除操作
download.handleClick(); // 暂停下载
download.hanldeDel(); // 删除任务
# 策略模式
策略模式定义:就是能够把一系列“可互换的”算法封装起来,并根据用户需求来选择其中一种。
# 什么是策略模式?
策略模式定义:就是能够把一系列“可互换的”算法封装起来,并根据用户需求来选择其中一种。
策略模式的实现核心就是:将算法的使用和算法的实现分离。算法的实现交给策略类。算法的使用交给环境类,环境类会根据不同的情况选择合适的算法。
# 策略模式优缺点
在使用策略模式的时候,需要了解所有的“策略”(strategy)之间的异同点,才能选择合适的“策略”进行调用。
代码实现
// 策略类
const strategies = {
A() {
console.log("This is stragegy A");
},
B() {
console.log("This is stragegy B");
}
};
// 环境类
const context = name => {
return strategies[name]();
};
// 调用策略A
context("A");
// 调用策略B
context("B");
# 解释器模式
解释器模式: 提供了评估语言的语法或表达式的方式。
# 什么是“解释器模式?
解释器模式定义: 提供了评估语言的语法或表达式的方式。
这是基本不怎么使用的一种设计模式。确实想不到什么场景一定要用此种设计模式。
实现这种模式的核心是:
- 抽象表达式:主要有一个interpret()操作
- 终结符表达式:R = R1 + R2中,R1 R2就是终结符
- 非终结符表达式:R = R1 - R2中,-就是终结符
- 环境(Context): 存放文法中各个终结符所对应的具体值。比如前面R1和R2的值。
# 优缺点
优点显而易见,每个文法规则可以表述为一个类或者方法。这些文法互相不干扰,符合“开闭原则”。
由于每条文法都需要构建一个类或者方法,文法数量上去后,很难维护。并且,语句的执行效率低(一直在不停地互相调用)。
class Context {
constructor() {
this._list = []; // 存放 终结符表达式
this._sum = 0; // 存放 非终结符表达式(运算结果)
}
get sum() {
return this._sum;
}
set sum(newValue) {
this._sum = newValue;
}
add(expression) {
this._list.push(expression);
}
get list() {
return [...this._list];
}
}
class PlusExpression {
interpret(context) {
if (!(context instanceof Context)) {
throw new Error("TypeError");
}
context.sum = ++context.sum;
}
}
class MinusExpression {
interpret(context) {
if (!(context instanceof Context)) {
throw new Error("TypeError");
}
context.sum = --context.sum;
}
}
/** 以下是测试代码 **/
const context = new Context();
// 依次添加: 加法 | 加法 | 减法 表达式
context.add(new PlusExpression());
context.add(new PlusExpression());
context.add(new MinusExpression());
// 依次执行: 加法 | 加法 | 减法 表达式
context.list.forEach(expression => expression.interpret(context));
console.log(context.sum);
# 订阅-发布模式
订阅-发布模式:定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。
# 什么是“订阅-发布模式”?
订阅-发布模式:定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。
了解过事件机制或者函数式编程的朋友,应该会体会到“订阅-发布模式”所带来的“时间解耦”和“空间解耦”的优点。借助函数式编程中闭包和回调的概念,可以很优雅地实现这种设计模式。
# “订阅-发布模式” vs 观察者模式
订阅-发布模式和观察者模式概念相似,但在订阅-发布模式中,订阅者和发布者之间多了一层中间件:一个被抽象出来的信息调度中心。
但其实没有必要太深究 2 者区别,因为《Head First 设计模式》这本经典书都写了:发布+订阅=观察者模式。其核心思想是状态改变和发布通知。在此基础上,根据语言特性,进行实现即可。
const Event = {
clientList: {},
// 绑定事件监听
listen(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
return true;
},
// 触发对应事件
trigger() {
const key = Array.prototype.shift.apply(arguments),
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (let fn of fns) {
fn.apply(null, arguments);
}
return true;
},
// 移除相关事件
remove(key, fn) {
let fns = this.clientList[key];
// 如果之前没有绑定事件
// 或者没有指明要移除的事件
// 直接返回
if (!fns || !fn) {
return false;
}
// 反向遍历移除置指定事件函数
for (let l = fns.length - 1; l >= 0; l--) {
let _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
return true;
}
};
// 为对象动态安装 发布-订阅 功能
const installEvent = obj => {
for (let key in Event) {
obj[key] = Event[key];
}
};
let salesOffices = {};
installEvent(salesOffices);
// 绑定自定义事件和回调函数
salesOffices.listen(
"event01",
(fn1 = price => {
console.log("Price is", price, "at event01");
})
);
salesOffices.listen(
"event02",
(fn2 = price => {
console.log("Price is", price, "at event02");
})
);
salesOffices.trigger("event01", 1000);
salesOffices.trigger("event02", 2000);
salesOffices.remove("event01", fn1);
// 输出: false
// 说明删除成功
console.log(salesOffices.trigger("event01", 1000));
# 责任链模式
责任链模式定义:多个对象均有机会处理请求,从而解除发送者和接受者之间的耦合关系。这些对象连接成为“链式结构”,每个节点转发请求,直到有对象处理请求为止。
其核心思想就是:请求者不必知道是谁哪个节点对象处理的请求。如果当前不符合终止条件,那么把请求转发给下一个节点处理。
# 什么是“责任链模式”?
责任链模式定义:多个对象均有机会处理请求,从而解除发送者和接受者之间的耦合关系。这些对象连接成为“链式结构”,每个节点转发请求,直到有对象处理请求为止。
其核心思想就是:请求者不必知道是谁哪个节点对象处理的请求。如果当前不符合终止条件,那么把请求转发给下一个节点处理。
而当需求具有“传递”的性质时(代码中其中一种体现就是:多个if、else if、else if、else嵌套),就可以考虑将每个分支拆分成一个节点对象,拼接成为责任链。
# 优点与代价
优点:
- 可以根据需求变动,任意向责任链中添加 / 删除节点对象
- 没有固定的“开始节点”,可以从任意节点开始
代价:责任链最大的代价就是每个节点带来的多余消耗。当责任链过长,很多节点只有传递的作用,而不是真正地处理逻辑。
class Handler {
constructor() {
this.next = null;
}
setNext(handler) {
this.next = handler;
}
}
class LogHandler extends Handler {
constructor(...props) {
super(...props);
this.name = "log";
}
handle(level, msg) {
if (level === this.name) {
console.log(`LOG: ${msg}`);
return;
}
this.next && this.next.handle(...arguments);
}
}
class WarnHandler extends Handler {
constructor(...props) {
super(...props);
this.name = "warn";
}
handle(level, msg) {
if (level === this.name) {
console.log(`WARN: ${msg}`);
return;
}
this.next && this.next.handle(...arguments);
}
}
class ErrorHandler extends Handler {
constructor(...props) {
super(...props);
this.name = "error";
}
handle(level, msg) {
if (level === this.name) {
console.log(`ERROR: ${msg}`);
return;
}
this.next && this.next.handle(...arguments);
}
}
/******************以下是测试代码******************/
let logHandler = new LogHandler();
let warnHandler = new WarnHandler();
let errorHandler = new ErrorHandler();
// 设置下一个处理的节点
logHandler.setNext(warnHandler);
warnHandler.setNext(errorHandler);
logHandler.handle("error", "Some error occur");
# 迭代器模式
迭代器模式是指提供一种方法顺序访问一个集合对象的各个元素,使用者不需要了解集合对象的底层实现。
# 什么是迭代器模式?
迭代器模式是指提供一种方法顺序访问一个集合对象的各个元素,使用者不需要了解集合对象的底层实现。
# 内部迭代器和外部迭代器
内部迭代器:封装的方法完全接手迭代过程,外部只需要一次调用。
外部迭代器:用户必须显式地请求迭代下一元素。熟悉 C++的朋友,可以类比 C++内置对象的迭代器的 end()、next()等方法。
ES6 实现
这里实现的是一个外部迭代器。需要实现边界判断函数、元素获取函数和更新索引函数。
const Iterator = obj => {
let current = 0;
let next = () => (current += 1);
let end = () => current >= obj.length;
let get = () => obj[current];
return {
next,
end,
get
};
};
let myIter = Iterator([1, 2, 3]);
while (!myIter.end()) {
console.log(myIter.get());
myIter.next();
}
← 目录 [学习资源]javaScript →