# 创建型模式: 创建对象的模式,抽象了实例化的过程

# 单例模式

# 什么是单例模式?

单例模式定义:保证一个类仅有一个实例,并提供访问此实例的全局访问点。

# 单例模式用途

如果一个类负责连接数据库的线程池、日志记录逻辑等等,此时需要单例模式来保证对象不被重复创建,以达到降低开销的目的。

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();

# 结构型模式:

解决怎样组装现有对象,设计交互方式,从而达到实现一定的功能目的。例如,以封装为目的的适配器和桥接,以扩展性为目的的代理、装饰器

# 享元模式

享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。

# 什么是“享元模式”?

享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。

  • 享元模式提醒我们将一个对象的属性划分为内部和外部状态。
    1. 内部状态:可以被对象集合共享,通常不会改变
    2. 外部状态:根据应用场景经常改变
  • 享元模式是利用时间换取空间的优化模式。
# 应用场景

享元模式虽然名字听起来比较高深,但是实际使用非常容易:只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。

为了显示它的强大,下面的代码是简单地实现了大家耳熟能详的“对象池”,以彰显这种设计模式的魅力。

# 代码实现

通过阅读下方代码可以发现:对于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");

# 解释器模式

解释器模式: 提供了评估语言的语法或表达式的方式。

# 什么是“解释器模式?

解释器模式定义: 提供了评估语言的语法或表达式的方式。

这是基本不怎么使用的一种设计模式。确实想不到什么场景一定要用此种设计模式。

实现这种模式的核心是:

  1. 抽象表达式:主要有一个interpret()操作
  2. 终结符表达式:R = R1 + R2中,R1 R2就是终结符
  3. 非终结符表达式:R = R1 - R2中,-就是终结符
  4. 环境(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嵌套),就可以考虑将每个分支拆分成一个节点对象,拼接成为责任链。

# 优点与代价

优点:

  1. 可以根据需求变动,任意向责任链中添加 / 删除节点对象
  2. 没有固定的“开始节点”,可以从任意节点开始

代价:责任链最大的代价就是每个节点带来的多余消耗。当责任链过长,很多节点只有传递的作用,而不是真正地处理逻辑。

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();
}