欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

掌握JavaScript的23种设计模式

最编程 2024-08-06 14:46:32
...

目录

前言

一、创建型模式

1、原型模式

2、单例模式

3、工厂模式

4、抽象工厂模式

5、建造者模式(生成器模式)

二、结构型模式

1、桥接模式

2、外观模式

3、享元模式

4、适配器模式

5、代理模式(委托模式)

(1)、正向代理和反向代理

(2)、虚拟代理

(3)、缓存代理

(4)、用 ES6 的 Proxy 构造函数实现代理

6、组合模式

7、装饰模式

三、行为型模式

1、观察者模式(发布/订阅模式)

2、迭代器模式

3、策略模式

4、模板方法模式

5、状态模式

6、命令模式(事务模式)

7、访问者模式

8、中介者模式(调停模式)

9、备忘录模式

10、解释器模式

11、职责链模式


前言

在创建一个模式时,一定要注意箭头函数的使用,使用不当会导致程序报错。

设计模式一共有 23 种(GoF 总结的 23 种设计模式),分三大类型: 5 种创建型,7 种结构型,11 种行为型等

  • 创建型模式
    • 单例模式
    • 抽象工厂模式
    • 工厂模式
    • 建造者模式(生成器模式)
    • 原型模式
  • 结构型模式
    • 适配器模式
    • 桥接模式
    • 装饰模式
    • 组合模式
    • 外观模式
    • 享元模式
    • 代理模式
  • 行为型模式
    • 模板方法模式
    • 命令模式
    • 迭代器模式
    • 观察者模式
    • 访问者模式
    • 中介者模式
    • 备忘录模式
    • 解释器模式
    • 状态模式
    • 策略模式
    • 职责链模式

一、创建型模式

1、原型模式

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。

function Person () {
    Person.prototype.name = "marry";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}
 
const person1 = new Person();
const person2 = new Person();
person1.sayName();                                        // marry
person2.sayName();                                        // marry
console.log(person1.sayName === person2.sayName);         // true

用同一个原型new出来的实例,拥有相同的原型上的属性和方法。 

【拓展】用构造函数创建函数时不可以使用箭头函数。

2、单例模式

单例模式(Singleton Pattern)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。

该模式的特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。
// 单例模式
let box;
const createBox = (_a, _b) => {
    if(!box){
        box = {};
    }
    box.a = _a;
    box.b = _b;
    return box;
};

const obj1 = createBox(3, 6);
obj1; // {a: 3, b: 6}

const obj2 = createBox(10, 20);
obj1; // {a: 10, b: 20}
obj2; // {a: 10, b: 20}

可见,单例模式可以创建多个实例,但是,只要改变其中任一实例对象的属性值,其他所有的实例对象的属性值都变了。所以,单例类只能有一个实例,否则就会出错。

【拓展】为什么改变单例模式的一个实例对象的属性值,其所有的实例对象的属性值都变了呢?

  • 对象的浅拷贝,拷贝的仅仅是“引用地址”,不是值。所以,无论改变哪个对象的值,另一个对象对应的值也会改变。
  • 对象的深拷贝,把引用地址和值一起拷贝过来,一个对象的值改变,另一个对象的值不受影响。

3、工厂模式

工厂模式:根据不同的输入返回不同类的实例,一般用来创建同一类对象。

工厂方式的主要思想是将对象的创建与对象的实现分离。

工厂模式的使用场景:

  • 对象的创建比较复杂,而访问者无需知道创建的具体流程;
  • 处理大量具有相同属性的小对象;

工厂模式的优缺点:

  • 优点:工厂模式将 对象的创建和实现分离。
    • 良好的封装,代码结构清晰,访问者无需知道对象的创建流程,特别是创建比较复杂的情况下;
    • 扩展性优良,通过工厂方法隔离了用户和创建流程隔离,符合开放封闭原则;
    • 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流;
  • 缺点:带来了额外的系统复杂度,增加了抽象性;

工厂模式与模板方法模式的主要区别是:

  • 工厂模式主要关注产品实例的创建,对创建流程封闭起来;
  • 模板方法模式 主要专注的是为固定的算法骨架提供某些步骤的实现;

【典例】点菜

// 饭店方法
function restaurant(menu) {
    switch (menu) {
        case '鱼香肉丝':
            return new YuXiangRouSi();
        case '宫保鸡丁':
            return new GongBaoJiDin();
        default:
            throw new Error('这个菜本店没有');
    }
};
 
// 鱼香肉丝类 
function YuXiangRouSi() { this.type = '鱼香肉丝' };
YuXiangRouSi.prototype.eat = function () {
    console.log(this.type + ' 真香');
};
 
// 宫保鸡丁类 
function GongBaoJiDin() { this.type = '宫保鸡丁' };
GongBaoJiDin.prototype.eat = function () {
    console.log(this.type + ' 让我想起了外婆做的菜~');
};
 
 
const dish1 = restaurant('鱼香肉丝');
dish1.eat();										
// 鱼香肉丝 真香
const dish2 = restaurant('红烧排骨');
// Error 这个菜本店没有

使用 ES6 的 class 语法改写:

// 饭店方法 
class Restaurant {
    static getMenu(menu) {
        switch (menu) {
            case '鱼香肉丝':
                return new YuXiangRouSi();
            case '宫保鸡丁':
                return new GongBaoJiDin();
            default:
                throw new Error('这个菜本店没有');
        }
    }
};
 
// 鱼香肉丝类 
class YuXiangRouSi {
    constructor() { 
        this.type = '鱼香肉丝' 
    }
    eat() { 
        console.log(this.type + ' 真香') 
    }
};
 
// 宫保鸡丁类
class GongBaoJiDin {
    constructor() { 
        this.type = '宫保鸡丁' 
    }
    eat() { 
        console.log(this.type + ' 让我想起了外婆做的菜'); 
    }
};
 
const dish1 = Restaurant.getMenu('鱼香肉丝');
dish1.eat();								 				 
// 鱼香肉丝 真香
const dish2 = Restaurant.getMenu('红烧排骨');	
// Error 这个菜本店没有 

这样就完成了一个工厂模式,但是这个实现有一个问题:工厂方法中包含了很多与创建产品相关的过程,如果产品种类很多的话,这个工厂方法中就会罗列很多产品的创建逻辑,每次新增或删除产品种类,不仅要增加产品类,还需要对应修改在工厂方法,违反了开闭原则,也导致这个工厂方法变得臃肿、高耦合。

严格上这种实现在面向对象语言中叫做 简单工厂模式 。适用于产品种类比较少,创建逻辑不复杂的时候使用。

工厂模式 的本意是将实际创建对象的过程推迟到子类中,一般用抽象类来作为父类,创建过程由抽象类的子类来具体实现。JavaScript 中没有抽象类,所以我们可以简单地将工厂模式看做是一个实例化对象的工厂类即可。关于抽象类的有关内容,可以参看抽象工厂模式。

然而作为灵活的 JavaScript,我们不必如此较真,可以把易变的参数提取出来:

// 饭店方法
class Restaurant {
 
    constructor() {
        this.menuData = {}
    }
    
    // 创建菜品
    getMenu(menu) {
        if (!this.menuData[menu]){
            throw new Error('这个菜本店没有')
        };
        const { type, message } = this.menuData[menu];
        return new Menu(type, message);
    }
    
    // 增加菜品
    addMenu(menu, type, message) {
        if (this.menuData[menu]) {
            console.Info('已经有这个菜了!')
            return
        };
        this.menuData[menu] = { type, message }
    }
    
    // 移除菜品
    removeMenu(menu) {
        if (!this.menuData[menu]) return
        delete this.menuData[menu]
    }
}
 
// 菜品类
class Menu {
    constructor(type, message) {
        this.type = type
        this.message = message
    }
    eat() { 
        console.log(this.type + this.message) 
    }
}
 
const restaurant = new Restaurant();
 
// 注册菜品
restaurant.addMenu('YuXiangRouSi', '鱼香肉丝', ' 真香');		
restaurant.addMenu('GongBaoJiDin', '宫保鸡丁', ' 让我想起了外婆做的菜');
 
const dish1 = restaurant.getMenu('YuXiangRouSi');
dish1.eat();	
// 鱼香肉丝 真香
const dish2 = restaurant.getMenu('HongSaoPaiGu');	
// Error 这个菜本店没有

【总结归纳】工厂模式的通用实现:

  • Factory :工厂,负责返回产品实例;
  • Product :产品,访问者从工厂拿到产品实例;

【拓展】箭头函数没有函数声明提升。所以上述代码中,对于 getRandomColor 方法,如果采用箭头函数,则必须将 for 循环代码移至 getRandomColor 方法下面,否则会报错;如果采用函数声明的方式创建 getRandomColor 方法,则不会报错。

4、抽象工厂模式

工厂模式:根据输入的不同返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离。

抽象工厂模式:通过对类的工厂抽象使其业务用于对产品类簇的创建,而不是负责创建某一类产品的实例。关键在于使用抽象类制定了实例的结构,调用者直接面向实例的结构编程,从实例的具体实现中解耦。

抽象工厂模式的优缺点:

  • 优点:抽象产品类将产品的结构抽象出来,访问者不需要知道产品的具体实现,只需要面向产品的结构编程即可,从产品的具体实现中解耦;
  • 缺点:
    • 扩展新类簇的产品类比较困难,因为需要创建新的抽象产品类,并且还要修改工厂类,违反开放封闭原则;
    • 带来了系统复杂度,增加了新的类,和新的继承关系;

抽象工厂模式的使用场景:如果一组实例都有相同的结构,那么就可以使用抽象工厂模式。

抽象工厂模式与工厂模式的区别:

  • 工厂模式 主要关注单独的产品实例的创建;
  • 抽象工厂模式 主要关注产品类簇实例的创建,如果产品类簇只有一个产品,那么这时的抽象工厂模式就退化为工厂模式了;

JavaScript 没有提供抽象类,但是可以模拟抽象类:

// 抽象类,ES6 class 方式 
class AbstractClass1 {
    constructor() {
        if (new.target === AbstractClass1) {
            throw new Error('抽象类不能直接实例化!')
        }
    };
    // 抽象方法
    operate() { throw new Error('抽象方法不能调用!') }
}
 
// 抽象类,ES5 构造函数方式 
var AbstractClass2 = function () {
    if (new.target === AbstractClass2) {
        throw new Error('抽象类不能直接实例化!')
    }
}
// 抽象方法,使用原型方式添加 
AbstractClass2.prototype.operate = function () { throw new Error('抽象方法不能调用!') }

【典例】点菜

// 饭店方法 
function Restaurant() {};
 
Restaurant.orderDish = function(type) {
    switch (type) {
        case '鱼香肉丝':
            return new YuXiangRouSi()
        case '宫保鸡丁':
            return new GongBaoJiDing()
        case '紫菜蛋汤':
            return new ZiCaiDanTang()
        default:
            throw new Error('本店没有这个')
    }
}
 
// 菜品抽象类
function Dish() { this.kind = '菜' }
Dish.prototype.eat = function() { throw new Error('抽象方法不能调用!') };
 
// 鱼香肉丝类 
function YuXiangRouSi() { this.type = '鱼香肉丝' };
YuXiangRouSi.prototype = new Dish();
YuXiangRouSi.prototype.eat = function() {
    console.log(this.kind + ' - ' + this.type + ' 真香~');
};
 
 
// 宫保鸡丁类 
function GongBaoJiDing() { this.type = '宫保鸡丁' };
GongBaoJiDing.prototype = new Dish();
GongBaoJiDing.prototype.eat = function() {
    console.log(this.kind + ' - ' + this.type + ' 让我想起了外婆做的菜');
};
 
 
const dish1 = Restaurant.orderDish('鱼香肉丝');
dish1.eat();
// 菜 - 鱼香肉丝 真香
const dish2 = Restaurant.orderDish('红烧排骨');
// Error 本店没有这个

用 class 语法改写一下:

// 饭店方法
class Restaurant {
    static orderDish(type) {
        switch (type) {
            case '鱼香肉丝':
                return new YuXiangRouSi();
            case '宫保鸡丁':
                return new GongBaoJiDin();
            default:
                throw new Error('本店没有这个')
        }
    }
}
 
// 菜品抽象类
class Dish {
    constructor() {
 
        if (new.target === Dish) {
            throw new Error('抽象类不能直接实例化!')
        }
        this.kind = '菜'
    }
    
    // 抽象方法
    eat() { throw new Error('抽象方法不能调用!') }
}
 
// 鱼香肉丝类
class YuXiangRouSi extends Dish {
    constructor() {
        super()
        this.type = '鱼香肉丝'
    }
    eat() { console.log(this.kind + ' - ' + this.type + ' 真香') }
}
 
// 宫保鸡丁类
class GongBaoJiDin extends Dish {
    constructor() {
        super();
        this.type = '宫保鸡丁';
    }
    eat() { console.log(this.kind + ' - ' + this.type + ' 让我想起了外婆做的菜') }
}
 
const dish0 = new Dish();  										
// Error 抽象类不能直接实例化
 
const dish1 = Restaurant.orderDish('鱼香肉丝');
dish1.eat();																	
// 菜 - 鱼香肉丝 真香
 
const dish2 = Restaurant.orderDish('红烧排骨');
// Error 本店没有这个

上面的实现将产品的功能结构抽象出来成为抽象产品类。事实上我们还可以更进一步,将工厂类也使用抽象类约束一下,也就是抽象工厂类,比如这个饭店可以做菜和汤,另一个饭店也可以做菜和汤,存在共同的功能结构,就可以将共同结构作为抽象类抽象出来,实现如下:

// 饭店 抽象类,饭店都可以做菜和汤
class AbstractRestaurant {
    constructor() {
        if (new.target === AbstractRestaurant) {
            throw new Error('抽象类不能直接实例化!')
        }
        this.signborad = '饭店'
    }
    // 抽象方法:创建菜
    createDish() { throw new Error('抽象方法不能调用!') }
 
    // 抽象方法:创建汤
    createSoup() { throw new Error('抽象方法不能调用!') }
}
 
// 饭店 具体类 
class Restaurant extends AbstractRestaurant {
 
    constructor() { super() }
    createDish(type) {
        switch (type) {
            case '鱼香肉丝':
                return new YuXiangRouSi();
            case '宫保鸡丁':
                return new GongBaoJiDing();
            default:
                throw new Error('本店没这个菜');
        }
    }
    createSoup(type) {
        switch (type) {
            case '紫菜蛋汤':
                return new ZiCaiDanTang();
            default:
                throw new Error('本店没这个汤');
        }
    }
}
 
 
// 菜 抽象类,菜都有吃的功能
class AbstractDish {
    constructor() {
        if (new.target === AbstractDish) {
            throw new Error('抽象类不能直接实例化!')
        }
        this.kind = '菜'
    }
 
    // 抽象方法
    eat() { throw new Error('抽象方法不能调用!') }
}
 
// 菜 鱼香肉丝类
class YuXiangRouSi extends AbstractDish {
    constructor() {
        super()
        this.type = '鱼香肉丝'
    }
    eat() { console.log(this.kind + ' - ' + this.type + ' 真香~') }
}
// 菜 宫保鸡丁类
class GongBaoJiDing extends AbstractDish {
    constructor() {
        super()
        this.type = '宫保鸡丁'
    }
    eat() { console.log(this.kind + ' - ' + this.type + ' 让我想起了外婆做的菜') }
}
 
 
 
 
 
// 汤 抽象类,汤都有喝的功能
class AbstractSoup {
    constructor() {
        if (new.target === AbstractDish) {
            throw new Error('抽象类不能直接实例化!')
        }
        this.kind = '汤'
    }
    // 抽象方法
    drink() { throw new Error('抽象方法不能调用!') }
}
 
// 汤 紫菜蛋汤类
class ZiCaiDanTang extends AbstractSoup {
    constructor() {
        super()
        this.type = '紫菜蛋汤'
    }
    drink() { console.log(this.kind + ' - ' + this.type + ' 我从小喝到大') }
}
 
 
 
const restaurant = new Restaurant();
 
const soup1 = restaurant.createSoup('紫菜蛋汤');
soup1.drink();
// 汤 - 紫菜蛋汤 我从小喝到大
const dish1 = restaurant.createDish('鱼香肉丝');
dish1.eat();
// 菜 - 鱼香肉丝 真香
const dish2 = restaurant.createDish('红烧排骨');
// Error 本店没有这个

【总结归纳】抽象工厂模式的通用实现:

  • Factory :工厂,负责返回产品实例;
  • AbstractFactory :虚拟工厂,制定工厂实例的结构;
  • Product:产品,访问者从工厂中拿到的产品实例,实现抽象类;
  • AbstractProduct :产品抽象类,由具体产品实现,制定产品实例的结构;
// 工厂 抽象类
class AbstractFactory {
    constructor() {
        if (new.target === AbstractFactory){
            throw new Error('抽象类不能直接实例化!')
        }
    }
 
    // 抽象方法
    createProduct() { throw new Error('抽象方法不能调用!') }
}
 
// 工厂 具体类
class Factory extends AbstractFactory {
    constructor() { super() }
    createProduct(type) {
        switch (type) {
            case 'Product1':
                return new Product1();
            case 'Product2':
                return new Product2();
            default:
                throw new Error('当前没有这个产品');
        }
    }
}
 
// 产品 抽象类
class AbstractProduct {
    constructor() {
        if (new.target === AbstractProduct){
            throw new Error('抽象类不能直接实例化!');
        }
        this.kind = '抽象产品类'
    }
    // 抽象方法
    operate() { throw new Error('抽象方法不能调用!') }
}
 
// 产品 具体类1 
class Product1 extends AbstractProduct {
    constructor() {
        super();
        this.type = 'Product1';
    }
 
    operate() { console.log(this.kind + ' - ' + this.type) }
}
 
// 产品 具体类2
class Product2 extends AbstractProduct {
    constructor() {
        super();
        this.type = 'Product2';
    }
 
    operate() { console.log(this.kind + ' - ' + this.type) }
}
 
const factory = new Factory();
const product1 = factory.createProduct1('Product1');
prod1.operate();
// 抽象产品类 - Product1
const product2 = factory.createProduct1('Product3');	
// Error 当前没有这个产品

【拓展】箭头函数没有原型属性,不能定义原型方法。所以在构造函数的原型上定义的函数不能使用箭头函数(比如:Agency.game、Game.prototype.getName),对象里面的方法可以使用箭头函数定义(比如:getName),但是有时会因为箭头函数 this 指向的问题抛出错误。比如:

const calculator = {
    array: [1, 2, 3],
    sum: () => {
        console.log(this === window); // => true
        return this.array.reduce((result, item) => result + item);
    }
};

console.log(this === window); // => true
calculator.sum();

// true
// true
// Uncaught TypeError: Cannot read property 'reduce' of undefined

上述代码之所以报错是因为:代码运行时,this.array 是未定义的,调用 calculator.sum 的时候,执行上下文里面的 this 仍然指向的是 window,this.array 等价于 window.array,显然后者是未定义的。

5、建造者模式(生成器模式)

参考:JavaScript 设计模式学习第十篇-建造者模式

建造者模式用于:分步构建一个复杂的对象,将一个复杂对象的 构建层与其表示层分离。若不是极其复杂的对象,应选择使用对象字面或工厂模式等方式创建对象。

实现原理:通常使用链式调用来进行建造过程,最后调用 build() 方法生成最终对象。

建造者模式的优缺点:

  • 优点:
    • 封装性好,创建和使用分离;
    • 扩展性好,建造类之间独立、一定程度上解耦。
  • 缺点:
    • 产生多余的Builder对象;
    • 产品内部发生变化,建造者都要修改,成本较大。

建造者模式的适用场景:

  • 相同的方法,不同的执行顺序,产生不一样的产品时,可以采用建造者模式;
  • 产品的组成部件类似,通过组装不同的组件获得不同产品时,可以采用建造者模式;

建造者模式 与 工厂模式 的区别:

  • 工厂模式关注的是创建的结果。
  • 建造者模式不仅得到了结果,同时也参与了创建的具体过程。

【典例】:假定我们需要建造一个车,车这个产品是由多个部件组成,车身、引擎、轮胎。汽车制造厂一般不会自己完成每个部件的制造,而是把部件的制造交给对应的汽车零部件制造商,自己只进行装配,最后生产出整车。整车的每个部件都是一个相对独立的个体,都具有自己的生产过程,多个部件经过一系列的组装共同组成了一个完整的车。

装配汽车的代码实现如下:

// 建造者,汽车部件厂家,提供具体零部件的生产
function CarBuilder({ color = 'white', weight = 0 }) {
    this.color = color;
    this.weight = weight;
};
 
// 生产部件,轮胎
CarBuilder.prototype.buildTyre = function (type) {
    switch (type) {
        case 'small':
            this.tyreType = '小号轮胎'
            this.tyreIntro = '正在使用小号轮胎'
            break
        case 'normal':
            this.tyreType = '中号轮胎'
            this.tyreIntro = '正在使用中号轮胎'
            break
        case 'big':
            this.tyreType = '大号轮胎'
            this.tyreIntro = '正在使用大号轮胎'
            break
    }
};
 
// 生产部件,发动机
CarBuilder.prototype.buildEngine = function (type) {
    switch (type) {
        case 'small':
            this.engineType = '小马力发动机'
            this.engineIntro = '正在使用小马力发动机'
            break
        case 'normal':
            this.engineType = '中马力发动机'
            this.engineIntro = '正在使用中马力发动机'
            break
        case 'big':
            this.engineType = '大马力发动机'
            this.engineIntro = '正在使用大马力发动机'
            break
    }
};
 
// 奔驰厂家,负责最终汽车产品的装配
function benChiDirector(tyre, engine, param) {
    var car = new CarBuilder(param);
    car.buildTyre(tyre);
    car.buildEngine(engine);
    return car
};
 
// 获得产品实例
var benchi = benChiDirector('small', 'big', { color: 'red', weight: '1600kg' });
 
console.log(benchi);
 
// {
//     color: "red"
//     engineIntro: "正在使用大马力发动机"
//     engineType: "大马力发动机"
//     tyreIntro: "正在使用小号轮胎"
//     tyreType: "小号轮胎"
//     weight: "1600kg"
// }

如果访问者希望获得另一个型号的车,比如有空调功能的车,那么我们只需要给 CarBuilder 的原型 prototype 上增加一个空调部件的建造方法,然后再新建一个新的奔驰厂家指挥者方法。

也可以使用 ES6 的写法改造一下:

// 建造者,汽车部件厂家,提供具体零部件的生产
class CarBuilder {
 
    constructor({ color = 'white', weight = 0 }) {
        this.color = color;
        this.weight = weight;
    }
    
    // 生产部件,轮胎
    buildTyre(type) {
        const tyre = {}
        switch (type) {
            case 'small':
                tyre.tyreType = '小号轮胎'
                tyre.tyreIntro = '正在使用小号轮胎'
                break
            case 'normal':
                tyre.tyreType = '中号轮胎'
                tyre.tyreIntro = '正在使用中号轮胎'
                break
            case 'big':
                tyre.tyreType = '大号轮胎'
                tyre.tyreIntro = '正在使用大号轮胎'
                break
        }
        this.tyre = tyre;
    }
    
    // 生产部件,发动机
    buildEngine(type) {
        const engine = {}
        switch (type) {
            case 'small':
                engine.engineType = '小马力发动机'
                engine.engineIntro = '正在使用小马力发动机'
                break
            case 'normal':
                engine.engineType = '中马力发动机'
                engine.engineIntro = '正在使用中马力发动机'
                break
            case 'big':
                engine.engineType = '大马力发动机'
                engine.engineIntro = '正在使用大马力发动机'
                break
        }
        this.engine = engine
    }
};
 
 
// 指挥者,负责最终汽车产品的装配
class BenChiDirector {
    constructor(tyre, engine, param) {
        const car = new CarBuilder(param);
        car.buildTyre(tyre);
        car.buildEngine(engine);
        return car;
    }
};
 
// 获得产品实例
const benchi = new BenChiDirector('small', 'big', { color: 'red', weight: '1600kg' });
 
console.log(benchi);
 
// {
//     color: "red",
//     engine: {engineType: "大马力发动机", engineIntro: "正在使用大马力发动机"},
//     tyre: {tyreType: "小号轮胎", tyreIntro: "正在使用小号轮胎"},
//     weight: "1600kg"
// }

这样将最终产品的创建流程使用链模式来实现,相当于将指挥者退化,指挥的过程通过链模式让用户自己实现,这样既增加了灵活性,装配过程也一目了然。如果希望扩展产品的部件,那么在建造者上增加部件实现方法,再适当修改链模式即可。

【总结归纳】建造者模式的通用实现

  • Director:指挥者,调用建造者中的部件具体实现进行部件装配,相当于整车组装厂,最终返回装配完毕的产品。
  • Builder: 建造者,含有不同部件的生产方式给指挥者调用,是部件真正的生产者,但没有部件的装配流程。
  • Product:产品,要返回给访问者的复杂对象。

下面是通用的实现。

首先使用 ES6 的 class 语法:

// 建造者,部件生产
class ProductBuilder {
    constructor(param) {
        this.param = param
    }
    // 生产部件,part1
    buildPart1() {
        // Part1 生产过程...
        this.part1 = 'part1'
 
    }
    // 生产部件,part2 
    buildPart2() {
        // Part2 生产过程...
        this.part2 = 'part2'
    }
}
 
// 指挥者,负责最终产品的装配
class Director {
    constructor(param) {
        const _product = new ProductBuilder(param);
        _product.buildPart1();
        _product.buildPart2();
        return _product;
    }
};
 
// 获得产品实例
const product = new Director('param');

 结合链模式:

// 建造者,汽车部件厂家
class CarBuilder {
    constructor(param) {
        this.param = param;
    }
    
    // 生产部件,part1 
    buildPart1() {
        this.part1 = 'part1';
        return this
    }
    
    // 生产部件,part2
    buildPart2() {
        this.part2 = 'part2';
        return this;
    }
}
 
// 汽车装配,获得产品实例
const benchi1 = new CarBuilder('param')
.buildPart1()
.buildPart2();
 
console.log(benchi1);
 
// {
//     param: "param"
//     part1: "part1"
//     part2: "part2"
// }

如果希望扩展实例的功能,那么只需要在建造者类的原型上增加一个实例方法,再返回 this 即可。

值得一提的是,结合链模式的建造者模式中,装配复杂对象的链式装配过程就是指挥者 Director 角色,只不过在链式装配过程中不再封装在具体指挥者中,而是由使用者自己确定装配过程。

二、结构型模式

1、桥接模式

桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。使用组合关系代替继承关系,降低抽象和实现两个可变维度的耦合度。

桥接模式的优缺点:

  • 优点:
    • 分离了抽象和实现部分,将实现层(DOM 元素事件触发并执行具体修改逻辑)和抽象层( 元素外观、尺寸部分的修改函数)解耦,有利于分层;
    • 提高了可扩展性,多个维度的部件*组合,避免了类继承带来的强耦合关系,也减少了部件类的数量;
    • 使用者不用关心细节的实现,可以方便快捷地进行使用;
  • 缺点:
    • 桥接模式要求两个部件没有耦合关系,否则无法独立地变化,因此要求正确的对系统变化的维度进行识别,使用范围存在局限性;
    • 桥接模式的引入增加了系统复杂度;

桥接模式的适用场景:

  • 如果产品的部件有独立的变化维度,可以考虑桥接模式;
  • 不希望使用继承,或因为多层次继承导致系统类的个数急剧增加的系统;
  • 产品部件的粒度越细,部件复用的必要性越大,可以考虑桥接模式;
     

【典例】演奏乐器

function Boy(instrument) {
    this.sayHi = function() {
        console.log('hi, 我是男生')
    }

    // 有一个功能叫playInstrument, 没有具体乐器
    this.playInstrument = function() {
        instrument.play()
    }
}
 
function Girl(instrument) {
    this.sayHi = function() {
        console.log('hi, 我是女生')
    }

    // 有一个功能叫playInstrument, 没有具体乐器
    this.playInstrument = function() {
        instrument.play()
    }
}
 
function Piano() {
    this.play = function() {
        console.log('钢琴开始演奏')
    }
}
 
function Guitar() {
    this.play = function() {
        console.log('吉他开始演奏')
    }
}
 
let piano = new Piano()
let guitar = new Guitar()
let pianoBoy = new Boy(piano)
pianoBoy.playInstrument()
let guitarGirl = new Girl(guitar)
guitarGirl.playInstrument()

2、外观模式

外观模式为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。

外观模式的用途:将一些复杂操作封装起来,并创建一个简单的接口用于调用。

外观模式的适用场景:

  • 维护设计粗糙和难以理解的遗留系统,或者系统非常复杂的时候,可以为这些系统设置外观模块,给外界提供清晰的接口,以后新系统只需与外观交互即可;
  • 你写了若干小模块,可以完成某个大功能,但日后常用的是大功能,可以使用外观来提供大功能,因为外界也不需要了解小模块的功能;
  • 团队协作时,可以给各自负责的模块建立合适的外观,以简化使用,节约沟通时间;
  • 如果构建多层系统,可以使用外观模式来将系统分层,让外观模块成为每层的入口,简化层间调用,松散层间耦合;

外观模式的优缺点:

  • 优点:
    • 访问者不需要再了解子系统内部模块的功能,而只需和外观交互即可,使得访问者对子系统的 使用变得简单 ,符合最少知识原则,增强了可移植性和可读性;
    • 减少了与子系统模块的直接引用,实现了访问者与子系统中模块之间的松耦合,增加了可维护性和可扩展性;
    • 通过合理使用外观模式,可以帮助我们更好地划分系统访问层次,比如把需要暴露给外部的功能集中到外观中,这样既方便访问者使用,也很好地隐藏了内部的细节,提升了安全性;
  • 缺点:
    • 不符合开闭原则,对修改关闭,对扩展开放,如果外观模块出错,那么只能通过修改的方式来解决问题,因为外观模块是子系统的唯一出口;
    • 不需要或不合理的使用外观会让人迷惑,过犹不及;

外观模式与中介者模式的区别:

  • 外观模式:封装子使用者对子系统内模块的直接交互,方便使用者对子系统的调用;
  • 中介者模式:封装子系统间各模块之间的直接交互,松散模块间的耦合;

【典例】假如 html 中有一个 div,很多地方都要控制它的显示隐藏,但是每次都写是比较累赘,所以我们提供一个函数来实现,代码如下:

function setBox(){
	var getId = document.getElementById('isShow');
	return {
		show : function(){
			getId.style.display = 'block';
		},
		hide : function(){
			getId.style.display = 'none';
		}
	}
}

3、享元模式

享元模式:运用共享技术来有效地支持大量细粒度对象的复用,以减少创建的对象的数量。通俗来讲,享元就是共享单元,比如现在流行的共享单车、共享充电宝等,他们的核心理念都是享元模式。

享元模式适用于以下场景:

  • 程序中使用大量的相似对象,造成很大的内存开销
  • 对象的大多数状态都可以变为外部状态,剥离外部状态之后,可以用相对较少的共享对象取代大量对象。

享元模式的优缺点:

  • 优点:
    • 由于减少了系统中的对象数量,提高了程序运行效率和性能,精简了内存占用,加快运行速度;
    • 外部状态相对独立,不会影响到内部状态,所以享元对象能够在不同的环境被共享;
  • 缺点:
    • 引入了共享对象,使对象结构变得复杂;
    • 共享对象的创建、销毁等需要维护,带来额外的复杂度(如果需要把共享对象维护起来的话);

【典例一】 享元模式优化图书管理:

// 书的属性
// id
// title
// author
// genre
// page count
// publisher id
// isbn
 
// 管理所需的额外属性
// checkout date
// checkout member
// due return date
// availability
 
// 享元(存储内部状态)
function Book(title, author, genre, pageCount, publisherId, isbn) {
    this.title = title;
    this.author = author;
    this.genre = genre;
    this.pageCount = pageCount;
    this.publisherId = publisherId;
    this.isbn = isbn;
}
 
// 享元工厂(创建/管理享元)
var BookFactory = (function() {
    var existingBooks = {};
    var existingBook = null;
 
    return {
        createBook: function(title, author, genre, pageCount, publisherId, isbn) {
            // 如果书籍已经创建,,则找到并返回
            // !!强制返回bool类型
            existingBook = existingBooks[isbn];
            if (!!existingBook) {
                return existingBook;
            }
            else {
                // 如果不存在选择创建该书的新实例并保存
                var book = new Book(title, author, genre, pageCount, publisherId, isbn);
                existingBooks[isbn] = book;
                return book;
            }
        }
    }
})();
 
// 客户端(存储外部状态)
var BookRecordManager = (function() {
    var bookRecordDatabase = {};
 
    return {
        // 添加新书到数据库
        addBookRecord: function(id, title, author, genre, pageCount, publisherId, isbn,
                checkoutDate, checkoutMember, dueReturnDate, availability) {
            var book = BookFactory.createBook(title, author, genre, pageCount, publisherId, isbn);
 
            bookRecordDatabase[id] = {
                checkoutMember: checkoutMember,
                checkoutDate: checkoutDate,
                dueReturnDate: dueReturnDate,
                availability: availability,
                book: book
            }
        },
        updateCheckStatus: function(bookId, newStatus, checkoutDate, checkoutMember, newReturnDate) {
            var record = bookRecordDatabase[bookId];
 
            record.availability = newStatus;
            record.checkoutDate = checkoutDate;
            record.checkoutMember = checkoutMember;
            record.dueReturnDate = newReturnDate;
        },
        extendCheckoutPeriod: function(bookId, newReturnDate) {
            bookRecordDatabase[bookId].dueReturnDate = newReturnDate;
        },
        isPastDue: function(bookId) {
            var currDate = new Date();
 
            return currDate.getTime() > Date.parse(bookRecordDatabase[bookId].dueReturnDate);
        }
    };
})();
 
 
// isbn号是书籍的唯一标识,以下三条只会创建一个book对象
BookRecordManager.addBookRecord(1, 'x', 'x', 'xx', 300, 10001, '100-232-32');   // new book
BookRecordManager.addBookRecord(1, 'xx', 'xx', 'xx', 300, 10001, '100-232-32');
BookRecordManager.addBookRecord(1, 'xxx', 'xxx', 'xxx', 300, 10001, '100-232-32');

【典例二】享元模式实现文件上传:

var Upload = function(uploadType) {
  this.uploadType = uploadType;
}
​
/* 删除文件(内部状态) */
Upload.prototype.delFile = function(id) {
  uploadManger.setExternalState(id, this);  // 把当前id对应的外部状态都组装到共享对象中
  // 大于3000k提示
  if(this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  }
  if(window.confirm("确定要删除文件吗?" + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom);
  }
}
​
/** 工厂对象实例化 
 *  如果某种内部状态的共享对象已经被创建过,那么直接返回这个对象
 *  否则,创建一个新的对象
 */
var UploadFactory = (function() {
  var createdFlyWeightObjs = {};
  return {
    create: function(uploadType) {
      if(createdFlyWeightObjs[uploadType]) {
        return createdFlyWeightObjs[uploadType];
      }
      return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
    }
  };
})();
​
/* 管理器封装外部状态 */
var uploadManger = (function() {
  var uploadDatabase = {};
​
  return {
    add: function(id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);
      var dom = document.createElement('div');
      dom.innerHTML = "<span>文件名称:" + fileName + ",文件大小:" + fileSize +"</span>"
              + "<button class='delFile'>删除</button>";
​
      dom.querySelector(".delFile").onclick = function() {
        flyWeightObj.delFile(id);
      };
      document.body.appendChild(dom);
​
      uploadDatabase[id] = {
        fileName: fileName,
        fileSize: fileSize,
        dom: dom
      };
​
      return flyWeightObj;
    },
    setExternalState: function(id, flyWeightObj) {
      var uploadData = uploadDatabase[id];
      for(var i in uploadData) {
        // 直接改变形参(新思路!!)
        flyWeightObj[i] = uploadData[i];
      }
    }
  };
})();
​
/*触发上传动作*/
var id = 0;
window.startUpload = function(uploadType, files) {
  for(var i=0,file; file = files[i++];) {
    var uploadObj = uploadManger.add(++id, uploadType, file.fileName, file.fileSize);
  }
};
​
/* 测试 */
startUpload("plugin", [
  {
    fileName: '1.txt',
    fileSize: 1000
  },{
    fileName: '2.txt',
    fileSize: 3000
  },{
    fileName: '3.txt',
    fileSize: 5000
  }
]);
startUpload("flash", [
  {
    fileName: '4.txt',
    fileSize: 1000
  },{
    fileName: '5.txt',
    fileSize: 3000
  },{
    fileName: '6.txt',
    fileSize: 5000
  }
]);

【案例三】享元模式 + 对象池技术优化页面渲染:

对象池,也是一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态的过程。

const books = new Array(10000).fill(0).map((v, index) => {
    return Math.random() > 0.5 ? {
              name: `计算机科学${index}`,
              category: '技术类'
            } : {
              name: `傲慢与偏见${index}`,
              category: '文学类类'
            }
  })
 
class FlyweightBook {
  constructor(category) {
    this.category = category
  }
   // 用于享元对象获取外部状态
   getExternalState(state) {
     for(const p in state) {
        this[p] = state[p]
     }
   }
   print() {
     console.log(this.name, this.category)
   }
}
// 然后定义一个工厂,来为我们生产享元对象
// 注意,这段代码实际上用了单例模式,每个享元对象都为单例, 因为我们没必要创建多个相同的享元对象
const flyweightBookFactory = (function() {
   const flyweightBookStore = {}
   return function (category) {
     if (flyweightBookStore[category]) {
       return flyweightBookStore[category]
     }
     const flyweightBook = new FlyweightBook(category)
     flyweightBookStore[category] = flyweightBook
     return flyweightBook
   }
})()
// DOM的享元对象
class Div {
  constructor() {
    this.dom = document.createElement("div")
  }
 getExternalState(extState, onClick) {
   // 获取外部状态
   this.dom.innerText = extState.innerText
   // 设置DOM位置
   this.dom.style.top = `${extState.seq * 22}px`
   this.dom.style.position = `absolute`
   this.dom.onclick = onClick
 }
 mount(container) {
    container.appendChild(this.dom)
 }
}
 
const divFactory = (function() {
   const divPool = []; // 对象池
   return function(innerContainer) {
       let div
       if (divPool.length <= 20) {
          div = new Div()
          divPool.push(div)
       } else {
          // 滚动行为,在超过20个时,复用池中的第一个实例,返回给调用者
          div = divPool.shift()
          divPool.push(div)
       }
       div.mount(innerContainer)
       return div
   }
})()
 
// 外层container,用户可视区域
const container = document.createElement("div")
// 内层container, 包含了所有DOM的总高度
const innerContainer = document.createElement("div")
container.style.maxHeight = '400px'
container.style.width = '200px'
container.style.border = '1px solid'
container.style.overflow = 'auto'
innerContainer.style.height = `${22 * books.length}px` // 由每个DOM的总高度算出内层container的高度
innerContainer.style.position = `relative`
container.appendChild(innerContainer)
document.body.appendChild(container)
 
function load(start, end) {
  // 装载需要显示的数据
  books.slice(start, end).forEach((bookData, index) => {
     // 先生产出享元对象
    const flyweightBook = flyweightBookFactory(bookData.category)
    const div = divFactory(innerContainer)
    // DOM的高度需要由它的序号计算出来
    div.getExternalState({innerText: bookData.name, seq: start + index}, () => {
      flyweightBook.getExternalState({name: bookData.name})
      flyweightBook.print()
    })
  })
}
 
load(0, 20)
let cur = 0 // 记录当前加载的首个数据
container.addEventListener('scroll', (e) => {
  const start = container.scrollTop / 22 | 0
  if (start !== cur) {
    load(start, start + 20)
    cur = start
  }
})

以上代码仅仅使用了2个享元对象,21个DOM对象,就完成了10000条数据的渲染,相比起建立10000个book对象和10000个DOM,性能优化是非常明显的。 

4、适配器模式

参考:JavaScript 设计模式学习第十三篇-适配器模式

适配器模式:将一个类(对象)的接口(方法、属性)转化为用户需要的另一个接口。解决类(对象)之间接口不兼容的问题。

适配器模式的优缺点:

  • 优点:
    • 已有的功能如果只是接口不兼容,使用适配器适配已有功能,可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码;
    • 可扩展性良好,在实现适配器功能的时候,可以调用自己开发的功能,从而方便地扩展系统的功能;
    • 灵活性好,因为适配器并没有对原有对象的功能有所影响,如果不想使用适配器了,那么直接删掉即可,不会对使用原有对象的代码有影响;
  • 缺点:会让系统变得零乱,明明调用 A,却被适配到了 B,如果系统中这样的情况很多,那么对可阅读性不太友好。如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,可以考虑尽量把文档完善。

适配器模式的适用场景:

  • 当你想用已有对象的功能,却想修改它的接口时,一般可以考虑一下是不是可以应用适配器模式。
  • 如果你想要使用一个已经存在的对象,但是它的接口不满足需求,那么可以使用适配器模式,把已有的实现转换成你需要的接口;
  • 如果你想创建一个可以复用的对象,而且确定需要和一些不兼容的对象一起工作,这种情况可以使用适配器模式,然后需要什么就适配什么;

适配器模式、代理模式以及装饰模式的区别:

  • 适配器模式: 原功能不变,只转换了原有接口访问格式;提供一个不一样的接口,由于原来的接口格式不能用了,提供新的接口以满足新场景下的需求;
  • 代理模式:原功能不变,但一般是经过限制访问的;提供一模一样的接口,由于不能直接访问目标对象,找个代理来帮忙访问,使用者可以就像访问目标对象一样来访问代理对象;
  • 装饰模式:扩展功能,原有功能不变且可直接使用;

【典例一】电源适配器

在中国,使用中国插头:

// 中国插头
var chinaPlug = {
    type: '中国插头',
    chinaInPlug() {
        console.log('开始供电')
    }
};

chinaPlug.chinaInPlug(); // 开始供电

出国旅游到了日本,需要增加一个日本插头到中国插头的电源适配器,来将我们原来的电源线用起来:

// 中国插头
var chinaPlug = {
    type: '中国插头',
    chinaInPlug() {
        console.log('开始供电');
    }
};

// 日本插头
var japanPlug = {
    type: '日本插头',
    japanInPlug() {
        console.log('开始供电');
    }
};

// 日本插头电源适配器
function japanPlugAdapter(plug) {
    return {
        chinaInPlug() {
            return plug.japanInPlug();
        }
    }
};
 
japanPlugAdapter(japanPlug).chinaInPlug(); // 开始供电

【典例二】数据的适配:将树形结构平铺成表形数据结构

// 原来的树形结构
const oldTreeData = [
    {
        name: '总部',
        place: '一楼',
        children: [
            { 
                name: '财务部', 
                place: '二楼' 
            },
            { 
                name: '生产部', 
                place: '三楼' 
            },
            {
                name: '开发部', 
                place: '三楼', 
                children: [
                    {
                        name: '软件部', 
                        place: '四楼', 
                        children: [
                            { name: '后端部', place: '五楼' },
                            { name: '前端部', place: '七楼' },
                            { name: '技术部', place: '六楼' }
                        ]
                    }, 
                    {
                        name: '硬件部', 
                        place: '四楼', 
                        children: [
                            { name: 'DSP部', place: '八楼' },
                            { name: 'ARM部', place: '二楼' },
                            { name: '调试部', place: '三楼' }
                        ]
                    }
                ]
            }
        ]
    }
];
 
// 树形结构平铺
function treeDataAdapter(treeData, lastArrayData = []) {
    treeData.forEach(item => {
        if (item.children) {
            treeDataAdapter(item.children, lastArrayData)
        }
        const { name, place } = item
        lastArrayData.push({ name, place })
    })
    return lastArrayData
};
// 返回平铺的组织结构
var data = treeDataAdapter(oldTreeData);

适配器模式也适用于适配后端接口返回的数据:

通常服务器端传递的数据和前端需要使用的数据格式不一致,这时需要对后端的数据格式进行适配。例如后端返回的数据格式为:

[
  {
    "day": "周一",
    "uv": 6300
  },
  {
    "day": "周二",
    "uv": 7100
  },  {
    "day": "周三",
    "uv": 4300
  },  {
    "day": "周四",
    "uv": 3300
  },  {
    "day": "周五",
    "uv": 8300
  },  {
    "day": "周六",
    "uv": 9300
  }, {
    "day": "周日",
    "uv": 11300
  }
]

但Echarts需要的x轴的数据格式和坐标点的数据是:

["周二", "周二", "周三", "周四", "周五", "周六", "周日"] //x轴的数据
 
[6300. 7100, 4300, 3300, 8300, 9300, 11300] //坐标点的数据

这时就可以使用适配器,将后端的返回数据做适配:

//x轴适配器
function echartXAxisAdapter(res) {
  return res.map(item => item.day);
}
 
//坐标点适配器
function echartDataAdapter(res) {
  return res.map(item => item.uv);
}

5、代理模式(委托模式)

代理模式:为其他对象提供一种代理以控制对这个对象的访问。

代理模式把代理对象插入到访问者和目标对象之间,从而为访问者对目标对象的访问引入一定的间接性。正是这种间接性,给了代理对象很多操作空间,比如在调用目标对象前和调用后进行一些预操作和后操作,从而实现新的功能或者扩展目标的功能。

代理模式的优缺点:

  • 优点:
    • 代理对象在访问者与目标对象之间可以起到 中介和保护目标对象 的作用;
    • 代理对象可以扩展目标对象的功能;
    • 代理模式能将访问者与目标对象分离,在一定程度上降低了系统的耦合度,如果我们希望适度扩展目标对象的一些功能,通过修改代理对象就可以了,符合开闭原则;
  • 缺点:增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式(十八线明星就别请经纪人了)。

代理模式与适配器模式的区别:

  • 适配器模式:主要用来解决接口之间不匹配的问题,通常是为所适配的对象提供一个不同的接口;
  • 代理模式:提供访问目标对象的间接访问,以及对目标对象功能的扩展,一般提供和目标对象一样的接口;

(1)、正向代理和反向代理

正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;

反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。常用于处理跨域请求。

(2)、虚拟代理

虚拟代理就是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。

比如:我们在浏览一些购物商城的时候,会发现,当网络不太好的情况下,有些图片是加载不出来的,会有暂无图片的一张图片去代替它实际的图片,等网路图片加载完成之后,暂无图片就会被实际的图片代替。这就是使用的图片的懒加载。图片的懒加载也可是使用虚拟代理的模式来进行设计:

// 图片懒加载
const myImage = (() {
    const imgNode = document.createElement('img');
    document.body.appendChild( imgNode );
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();

const proxyImage = (() {
    const img = new Image();
    img.onload = () => {
        myImage.setSrc( this.src );
    }
    return {
        setSrc: src => {
            myImage.setSrc('http://seopic.699pic.com/photo/40167/3716.jpg_wh1200.jpg');
            img.src = src;
        }
    }
})();

proxyImage.setSrc('http://seopic.699pic.com/photo/40167/7823.jpg_wh1200.jpg');

(3)、缓存代理

缓存代理就是可以为一些开销大的运算结果提供暂时的存储,下次运算时,如果传递进来堵塞参数跟之前一致,则可以直接返回前面存储的运算结果。

比如,前后端分离,向后端请求分页的数据的时候,每次页码改变时都需要重新请求后端数据,我们可以将页面和对应的结果进行缓存,当请求同一页的时候,就不再请求后端的接口而是从缓存中去取数据。


const getFib = (number) => {
    if (number <= 2) {
        return 1;
    } else {
        return getFib(number - 1) + getFib(number - 2);
    }
}

const getCacheProxy = (fn, cache = new Map()) => {
    return new Proxy(fn, {
        apply(target, context, args) {
        const argsString = args.join(' ');
        if (cache.has(argsString)) {
            // 如果有缓存,直接返回缓存数据 
            console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
            return cache.get(argsString);
        }
        const result = fn(...args);
        cache.set(argsString, result);
        return result;
        }
    })
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); 

(4)、用 ES6 的 Proxy 构造函数实现代理

ES6 所提供 Proxy 构造函数能够让我们轻松的使用代理模式:

const proxy = new Proxy(target, handler);

Proxy 构造函数传入两个参数:要代理的对象 和 用来定制代理行为的对象。(如果想知道 Proxy 的具体使用方法,可参考阮一峰的《 ECMAScript入门 - Proxy 》

6、组合模式

组合模式允许你将对象组合成树形结构来表现整体和部分的层次结构,让使用者可以以一致的方式处理组合对象以及部分对象。

组合模式的适用场景:如果对象组织呈树形结构就可以考虑使用组合模式,特别是如果操作树中对象的方法比较类似时。

组合模式的优缺点:

  • 优点
    • 忽略组合对象和单个对象的差别,对外一致接口使用;
    • 解耦调用者与复杂元素之间的联系,处理方式变得简单。
  • 缺点
    • 树叶对象接口一致,无法区分,只有在运行时方可辨别;
    • 包裹对象创建太多,额外增加内存负担。

典型的组合模式——文件夹。

【典例】用组合模式实现文件夹

// 创建文件夹
var createFolder = function (name) {
    return {
        name: name,
        _children: [],
 
        // 在文件夹下增加文件或文件夹
        add(fileOrFolder) {
            this._children.push(fileOrFolder)
        },
 
        // 扫描方法
        scan(cb) {
            this._children.forEach(function (child) {
                child.scan(cb)
            })
        }
    }
}
 
// 创建文件
var createFile = function (name, size) {
    return {
        name: name,
        size: size,
 
        // 在文件下增加文件,应报错
        add() {
            throw new Error('文件下面不能再添加文件')
        },
 
        // 执行扫描方法
        scan(cb) {
            cb(this)
        }
    }
}
 
// 创建总文件夹
var foldMovies = createFolder('电影')
 
// 创建子文件夹,并放入根文件夹
var foldMarvelMovies = createFolder('漫威英雄电影')
foldMovies.add(foldMarvelMovies)
 
var foldDCMovies = createFolder('DC英雄电影')
foldMovies.add(foldDCMovies)
 
// 为两个子文件夹分别添加电影
foldMarvelMovies.add(createFile('钢铁侠.mp4', 1.9))
foldMarvelMovies.add(createFile('蜘蛛侠.mp4', 2.1))
foldMarvelM