目录
前言
一、创建型模式
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)) foldMarvelMovies.add(createFile('金刚狼.mp4', 2.3)) foldMarvelMovies.add(createFile('黑寡妇.mp4', 1.9)) foldMarvelMovies.add(createFile('美国队长.mp4', 1.4)) foldDCMovies.add(createFile('蝙蝠侠.mp4', 2.4)) foldDCMovies.add(createFile('超人.mp4', 1.6)) console.log('size 大于2G的文件有:') foldMovies.scan(function (item) { if (item.size > 2) { console.log('name:' + item.name + ' size:' + item.size + 'GB') } }) // size 大于2G的文件有: // name:蜘蛛侠.mp4 size:2.1GB // name:金刚狼.mp4 size:2.3GB // name:蝙蝠侠.mp4 size:2.4GB
使用链模式进行改造:
// 创建文件夹 const createFolder = function (name) { return { name: name, _children: [], // 在文件夹下增加文件或文件夹 add(...fileOrFolder) { this._children.push(...fileOrFolder) return this }, // 扫描方法 scan(cb) { this._children.forEach(child => child.scan(cb)) } } } // 创建文件 const createFile = function (name, size) { return { name: name, size: size, // 在文件下增加文件,应报错 add() { throw new Error('文件下面不能再添加文件') }, // 执行扫描方法 scan(cb) { cb(this) } } } const foldMovies = createFolder('电影') .add( createFolder('漫威英雄电影') .add(createFile('钢铁侠.mp4', 1.9)) .add(createFile('蜘蛛侠.mp4', 2.1)) .add(createFile('金刚狼.mp4', 2.3)) .add(createFile('黑寡妇.mp4', 1.9)) .add(createFile('美国队长.mp4', 1.4)), createFolder('DC英雄电影') .add(createFile('蝙蝠侠.mp4', 2.4)) .add(createFile('超人.mp4', 1.6)) ) console.log('size 大于2G的文件有:'); foldMovies.scan(item => { if (item.size > 2) { console.log(`name:${item.name} size:${item.size}GB`) } }) // size 大于2G的文件有: // name:蜘蛛侠.mp4 size:2.1GB // name:金刚狼.mp4 size:2.3GB // name:蝙蝠侠.mp4 size:2.4GB
使用 ES6 的 class 语法来改写:
讯享网// 文件夹类 class Folder { constructor(name, children) { this.name = name this.children = children } // 在文件夹下增加文件或文件夹 add(...fileOrFolder) { this.children.push(...fileOrFolder) return this } // 扫描方法 scan(cb) { this.children.forEach(child => child.scan(cb)) } } // 文件类 class File { constructor(name, size) { this.name = name this.size = size } // 在文件下增加文件,应报错 add(...fileOrFolder) { throw new Error('文件下面不能再添加文件') } // 执行扫描方法 scan(cb) { cb(this) } } const foldMovies = new Folder('电影', [ new Folder('漫威英雄电影', [ new File('钢铁侠.mp4', 1.9), new File('蜘蛛侠.mp4', 2.1), new File('金刚狼.mp4', 2.3), new File('黑寡妇.mp4', 1.9), new File('美国队长.mp4', 1.4)]), new Folder('DC英雄电影', [ new File('蝙蝠侠.mp4', 2.4), new File('超人.mp4', 1.6)]) ]) console.log('size 大于2G的文件有:') foldMovies.scan(item => { if (item.size > 2) { console.log(`name:${ item.name } size:${ item.size }GB`) } }) // size 大于2G的文件有: // name:蜘蛛侠.mp4 size:2.1GB // name:金刚狼.mp4 size:2.3GB // name:蝙蝠侠.mp4 size:2.4GB
7、装饰器模式
可以将装饰器理解为游戏人物购买的装备,例如LOL中的英雄刚开始游戏时只有基础的攻击力和法强。但是在购买的装备后,在触发攻击和技能时,能够享受到装备带来的输出加成。我们可以理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。
装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。此模式可用于将特征添加到对象中,而无需修改底层的代码。
当我们接手老代码,需要对它已有的功能做个拓展。
var horribleCode = function(){ console.log('我是一堆老逻辑') } // 改成: var horribleCode = function(){ console.log('我是一堆老逻辑') console.log('我是新的逻辑') }
这样做有很多的问题。直接去修改已有的函数体,违背了我们的“开放封闭原则”;往一个函数体里塞这么多逻辑,违背了我们的“单一职责原则”。
为了不被已有的业务逻辑干扰,将旧逻辑与新逻辑分离,把旧逻辑抽出去:
讯享网var horribleCode = function(){ console.log('我是一堆老逻辑') } var _horribleCode = horribleCode horribleCode = function() { _horribleCode() console.log('我是新的逻辑') } horribleCode()
用 ES6 改写:
// 把原来的老逻辑代码放在一个类里 class HorribleCode () { control() { console.log('我是一堆老逻辑') } } // 老代码对应的装饰器 class Decorator { // 将老代码实例传入 constructor(olHC) { this.oldHC = oldHC } control() { this.oldHC.control() // “包装”了一层新逻辑 this.newHC() } newHC() { console.log('我是新的逻辑') } } const horribleCode = new HorribleCode() //这里我们把老代码实例传给了 Decorator,以便于后续 Decorator 可以进行逻辑的拓展。 const decorator = new Decorator(horribleCode) decorator.control()
ES7 为我们提供了语法糖可以给一个类装上装饰器,继续改造上面的代码:
讯享网// 装饰器函数,它的第一个参数是目标类 function Decorator(target, name, descriptor) { let originalMethod = descriptor.value descriptor.value = function() { console.log('我是Func的装饰器逻辑') console.log('我是新的逻辑') return originalMethod.apply(this, arguments) } return descriptor } class HorribleCode { @Decorator // 将装饰器“安装” 到HorribleCode上 control() { console.log('我是一堆老逻辑') } } // 验证装饰器是否生效 const horribleCode = new HorribleCode() horribleCode.control()
三、行为型模式
1、观察者模式(发布/订阅模式)
观察者模式又叫发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
观察者模式的优缺点:
- 优点:解耦。
- 时间上的解耦:注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
- 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体;
- 缺点:
- 增加消耗:创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
- 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的 deps/subs/watchers
- 缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布 - 订阅时,情况会变得更复杂。
观察者模式的使用场景:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
【典例】DOM 的 click 事件
div.onclick = function(){ alert ( “click” ); }
上述代码中,只要 div 订阅了的 click 事件,当点击 div 的时候,就会触发 click 事件。
自己写一个观察者模式:
讯享网function Journal(){ const fnList = []; return { //订阅 subscribe: (fn) => { const index = fnList.indexOf(fn); if(index!=-1) return fnList; fnList.push(fn); return fnList; }, //退订 unsubscribe: (fn) => { const index = fnList.indexOf(fn); if(index==-1) return fnList; fnList.splice(index, 1); return fnList; }, //发布 notify: () => { fnList.forEach(item => { item.update(); }); } } } const o = new Journal(); // 创建订阅者 function Observer(person, data) { return { update: () => { console.log(`${person}:${data}`); } } } const f1 = new Observer("张三", "今天天气不错"); const f2 = new Observer("李四", "我吃了三个汉堡"); const f3 = new Observer("王二", "你长得可真好看"); // f1,f2,f3订阅了 o.subscribe(f1); o.subscribe(f2); o.subscribe(f3); //f1取消了订阅 o.unsubscribe(f1); //发布 o.notify(); // 李四:我吃了三个汉堡 // 王二:你长得可真好看
2、迭代器模式
迭代器模式:用于顺序地访问聚合对象内部的元素,又无需知道对象内部结构。使用了迭代器之后,使用者不需要关心对象的内部构造,就可以按序访问其中的每个元素。
【典例】点钞机
银行里的点钞机就是一个迭代器,放入点钞机的钞票里有不同版次的人民币,每张钞票的冠字号也不一样,但当一沓钞票被放入点钞机中,使用者并不关心这些差别,只关心钞票的数量,以及是否有假币。
var bills = ['MCK0', 'MCK0', 'MCK0', 'MCK0', 'MCK0']; bills.forEach(function(bill) { console.log('当前钞票的冠字号为 ' + bill) })
如何实现一个迭代器呢,我们可以使用 for 循环自己实现一个 forEach:
讯享网const myForEach = (arr, callback) => { for ( let i = 0, l = arr.length; i < l; i++ ){ callback.call(arr, i, arr[i]); // 把下标和元素当作参数传给callback 函数 } }; const arr = ["a", "b", "c"]; myForEach(arr, (i, n) => { console.log( '自定义下标为: '+ i,'自定义值为:' + n ); }); // 自定义下标为: 0 自定义值为:a // 自定义下标为: 1 自定义值为:b // 自定义下标为: 2 自定义值为:c
3、策略模式
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式的优缺点:
- 优点:
- 策略之间相互独立,但策略可以自由切换 ,这个策略模式的特点给策略模式带来很多灵活性,也提高了策略的复用率;
- 如果不采用策略模式,那么在选策略时一般会采用多重的条件判断,采用策略模式可以 避免多重条件判断,增加可维护性;
- 可扩展性好,策略可以很方便的进行扩展;
- 缺点:
- 策略相互独立,因此一些复杂的算法逻辑 无法共享,造成一些资源浪费;
- 如果用户想采用什么策略,必须了解策略的实现,因此,所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也增加了用户对策略对象的使用成本;
策略模式的适用场景:
- 多个算法 只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法;
- 算法需要自由切换的场景;
- 有时 需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况;
策略模式和模板方法模式的区别:
- 策略模式 让我们在程序运行的时候动态地指定要使用的算法;
- 模板方法模式 是在子类定义的时候就已经确定了使用的算法;
【典例】薪资
使用策略模式之前:
var calculateBonus = function( performanceLevel, salary ){ if ( performanceLevel === 'S' ){ return salary * 4; } if ( performanceLevel === 'A' ){ return salary * 3; } if ( performanceLevel === 'B' ){ return salary * 2; } }; calculateBonus( 'B', 20000 ); // 输出:40000 calculateBonus( 'S', 6000 ); // 输出:24000
使用策略模式优化:
讯享网var strategies = { "S": function( salary ){ return salary * 4; }, "A": function( salary ){ return salary * 3; }, "B": function( salary ){ return salary * 2; } }; var calculateBonus = function( level, salary ){ return strategies[ level ]( salary ); }; console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000 console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000
【总结归纳】策略模式的通用实现
- Context :封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;
- Strategy :策略,含有具体的算法,其方法的外观相同,因此可以互相代替;
- StrategyMap :所有策略的合集,供封装上下文调用;
4、模板方法模式
模板方法模式:父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时,重新定义算法中的某些实现步骤。模板方法模式的关键是算法步骤的骨架和具体实现分离。
模板方法模式的优缺点:
- 优点:
- 封装了不变部分,扩展可变部分,把算法中不变的部分封装到父类中直接实现,而可变的部分由子类继承后再具体实现;
- 提取了公共代码部分,易于维护,因为公共的方法被提取到了父类,那么如果我们需要修改算法中不变的步骤时,不需要到每一个子类中去修改,只要改一下对应父类即可;
- 行为被父类的模板方法固定,子类实例只负责执行模板方法,具备可扩展性,符合开闭原则;
- 缺点:增加了系统复杂度,主要是增加了的抽象类和类间联系,需要做好文档工作;
模板方法模式的使用场景:
- 如果知道一个算法所需的关键步骤,而且很明确这些步骤的执行顺序,但是具体的实现是未知的、灵活的,那么这时候就可以使用模板方法模式来将算法步骤的框架抽象出来;
- 重要而复杂的算法,可以把核心算法逻辑设计为模板方法,周边相关细节功能由各个子类实现;
- 模板方法模式可以被用来将子类组件将自己的方法挂钩到高层组件中,也就是钩子,子类组件中的方法交出控制权,高层组件在模板方法中决定何时回调子类组件中的方法,类似的用法场景还有发布-订阅模式、回调函数;
模板方法模式与工厂模式的区别:
- 抽象工厂模式 提取的是实例的功能结构;
- 模板方法模式 提取的是算法的骨架结构;
模板方法模式与策略模式的区别:
- 模板方法模式 是在子类定义的时候就已经确定了使用的算法;
- 策略模式 让我们在程序运行的时候动态地指定要使用的算法;
【典例】泡咖啡和泡茶
泡咖啡和泡茶主要有以下不同点。
- 原料不同。一个是咖啡,一个是茶,但我们可以把它们都抽象 为“饮料”。
- 泡的方式不同。咖啡是冲泡,而茶叶是浸泡,我们可以把它们都抽 象为“泡”。
- 加入的调料不同。一个是糖和牛奶,一个是柠檬,但我们可以把它 们都抽象为“调料”。
经过抽象之后,不管是泡咖啡还是泡茶,我们都能整理为下面四步:
- 把水煮沸
- 用沸水冲泡饮料
- 把饮料倒进杯子
- 加调料
所以,不管是冲泡还是浸泡,我们都能给它一个新的方法名称,比如说brew()。同理,不管是加糖和牛奶,还是加柠檬,我们都可以称之为 addCondiments()。
var Beverage = function(){}; Beverage.prototype.boilWater = function(){ console.log( '把水煮沸' ); }; Beverage.prototype.brew = function(){ throw new Error( '子类必须重写brew方法' ); }; Beverage.prototype.pourInCup = function(){ throw new Error( '子类必须重写pourInCup方法' ); }; Beverage.prototype.addCondiments = function(){ throw new Error( '子类必须重写addCondiments方法' ); }; Beverage.prototype.customerWantsCondiments = function(){ return true; // 默认需要调料 }; Beverage.prototype.init = function(){ this.boilWater(); this.brew(); this.pourInCup(); if ( this.customerWantsCondiments() ){ // 如果挂钩返回true,则需要调料 this.addCondiments(); } }; var CoffeeWithHook = function(){}; CoffeeWithHook.prototype = new Beverage(); CoffeeWithHook.prototype.brew = function(){ console.log( '用沸水冲泡咖啡' ); }; CoffeeWithHook.prototype.pourInCup = function(){ console.log( '把咖啡倒进杯子' ); }; CoffeeWithHook.prototype.addCondiments = function(){ console.log( '加糖和牛奶' ); }; CoffeeWithHook.prototype.customerWantsCondiments = function(){ return window.confirm( '请问需要调料吗?' ); }; var coffeeWithHook = new CoffeeWithHook(); coffeeWithHook.init();
【总结归纳】模板方法模式的通用实现
- AbstractClass:抽象父类,把一些共用的方法提取出来,把可变的方法作为抽象类,最重要的是把算法骨架抽象出来为模板方法;
- templateMethod :模板方法,固定了希望执行的算法骨架;
- ConcreteClass :子类,实现抽象父类中定义的抽象方法,调用继承的模板方法时,将执行模板方法中定义的算法流程;
5、状态模式
状态模式:当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象。
状态模式的优缺点:
- 优点:
- 结构相比之下清晰,避免了过多的 switch-case 或 if-else 语句的使用,避免了程序的复杂性提高系统的可维护性;
- 符合开闭原则,每个状态都是一个子类,增加状态只需增加新的状态类即可,修改状态也只需修改对应状态类就可以了;
- 封装性良好,状态的切换在类的内部实现,外部的调用无需知道类内部如何实现状态和行为的变换;
- 缺点:引入了多余的类,每个状态都有对应的类,导致系统中类的个数增加。
状态模式的适用场景:
- 操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态,那么可以使用状态模式来将分支的处理分散到单独的状态类中;
- 对象的行为随着状态的改变而改变,那么可以考虑状态模式,来把状态和行为分离,虽然分离了,但是状态和行为是对应的,再通过改变状态调用状态对应的行为;
【典例】红绿灯
讯享网var trafficLight = (function () { var currentLight = null; return { change: function (light) { currentLight = light; currentLight.go(); } } })(); function RedLight() { } RedLight.prototype.go = function () { console.log("红灯"); } function GreenLight() { } GreenLight.prototype.go = function () { console.log("绿灯"); } function YellowLight() { } YellowLight.prototype.go = function () { console.log("黄灯"); } trafficLight.change(new RedLight()); trafficLight.change(new YellowLight());
6、命令模式(事务模式)
命令模式的原理:将请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
命令模式用于:将请求封装成对象,将命令的发送者和接受者解耦。即:将调用对象(用户界面、API 和代理等)与实现操作的对象隔离开。
命令模式的使用场景:对行为进行"记录、撤销/重做、事务"等处理,需要行为请求者与行为实现者解耦的时候(凡是两个对象间互动方式需要有更高的模块化程度时都可以用到这种模式)
命令模式的优缺点:
- 优点:
- 降低对象之间的耦合度。
- 新的命令可以很容易地加入到系统中。
- 可以比较容易地设计一个组合命令。
- 调用同一方法实现不同的功能。
- 缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
【典例】
// 命令 —— 执行命令(execute )时 ,便会执行各自命令接收者的action方法 var CreateCommand = function( receiver ){ this.receiver = receiver; } CreateCommand.prototype.execute = function() { this.receiver.action(); } // 接收者——电视——打开电视 var TVOn = function() {} TVOn.prototype.action = function() { alert("TVOn"); } // 接收者——电视——关闭电视 var TVOff = function() {} TVOff.prototype.action = function() { alert("TVOff"); } // 调用者——遥控器 var Invoker = function( tvOnCommand, tvOffCommand ) { this.tvOnCommand = tvOnCommand; this.tvOffCommand = tvOffCommand; } Invoker.prototype.tvOn = function() { this.tvOnCommand.execute(); } Invoker.prototype.tvOff = function() { this.tvOffCommand.execute(); } // 执行命令 var tvOnCommand = new CreateCommand( new TVOn() ); var tvOffCommand = new CreateCommand( new TVOff() ); var invoker = new Invoker( tvOnCommand, tvOffCommand ); invoker.tvOn(); invoker.tvOff();
7、访问者模式
访问者模式,针对于对象结构中的元素,定义在不改变该对象的前提下访问其结构中元素的新方法。
访问者模式由 3 部分构成:对象集合、集合元素、访问者。
访问者模式的应用场景:
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作变化影响对象的结构。
访问者模式的优缺点:
- 优点:
- 扩展性好:在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能
- 复用性好:通过访问者来定义整个对象结构通用的功能,从而提高复用程度
- 分离无关行为:通过访问者分离无关行为,把相关行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一
- 缺点:
- 被访问的类的结构是固定的,如果被访问的类的结构会发生变化,则不适合访问者模式
- 对象结构变化很困难:在访问者模式中,每增加一个新的元素类,都要在每一个具体的访问类中增加响应的具体操作,这违背了开闭原则
- 违反了依赖倒置原则:访问者模式依赖了具体类,而没有抽象类
【典例】
有一台电脑,有三部分组成,CPU、存储器、主板。
有三种客户:学生、上班族、公司,针对不同的客户采用不同的优惠策略。
首先分别实现 CPU、存储器、主板类组装成电脑,当访问者来的时候,电脑调用接待方法,CPU、存储器、主板分别接待访问者,访问者自己实现访问 CPU、存储器、主板的方法。CPU、存储器、主板的接待方法只需要接收一个访问者,调用对应的访问方法即可。
讯享网// 电脑部件——CPU let CPU = function(){ this.price = 10; } CPU.prototype.getPrice = function () { return this.price; } CPU.prototype.accept = function (v){ v.visitCpu(this); } // 电脑部件——存储器 let Memery = function (){ this.price = 15 } Memery.prototype.getPrice = function () { return this.price; } Memery.prototype.accept = function(v){ v.visitMemery(this); } // 电脑部件——主板 let Board = function(){ this.price = 20; } Board.prototype.getPrice = function (){ return this.price; } Board.prototype.accept = function(v){ v.visitBoard(this); } // 电脑——将CPU、存储器、主板组装成电脑 let Computer = function (){ this.cpu= new CPU(); this.memery = new Memery(); this.board = new Board(); } Computer.prototype.accept = function (v){ this.cpu.accept(v); this.memery.accept(v); this.board.accept(v); } // 访问者(客户)——学生 let studentVisitor = function() { this.totalPrice = 0; } studentVisitor.prototype.visitCpu = function (cpu) { this.totalPrice += cpu.getPrice() * 0.9; // 学生买电脑的 CPU 给打 9 折 } studentVisitor.prototype.visitMemery = function (memery){ this.totalPrice += memery.getPrice()*0.95; // 学生买电脑的存储器给打 95 折 } studentVisitor.prototype.visitBoard = function (board){ this.totalPrice += board.getPrice() * 0.8; // 学生买电脑的主板给打 8 折 }
8、中介者模式(调停模式)
中介者模式:用一个中介对象来封装多个对象之间的复杂交互。中介者将对象与对象之间紧密的耦合关系变得松散,从而可以独立地改变他们。
中介者模式用于解除对象与对象之间的紧耦合关系。
中介者模式的使用场景:如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
中介者模式的优缺点:
- 优点:
- 松散耦合,降低了同事对象之间的相互依赖和耦合,不会像之前那样牵一发动全身;
- 将同事对象间的一对多关联转变为一对一的关联,符合最少知识原则,提高系统的灵活性,使得系统易于维护和扩展;
- 中介者在同事对象间起到了控制和协调的作用,因此可以结合代理模式那样,进行同事对象间的访问控制、功能扩展;
- 因为同事对象间不需要相互引用,因此也可以简化同事对象的设计和实现;
- 缺点:
- 逻辑过度集中化,当同事对象太多时,中介者的职责将很重,逻辑变得复杂而庞大,以至于难以维护;
- 当出现中介者可维护性变差的情况时,考虑是否在系统设计上不合理,从而简化系统设计,优化并重构,避免中介者出现职责过重的情况;
【典例】相亲
首先我们考虑一个场景,男方和女方都有一定的条件,双方之间有要求,双方家长对对方孩子也有要求,如果达不到要求则不同意这门婚事。
class Person { // 个人信息 constructor(name, info, target) { this.name = name // 对象类型,每一项为数字,比如身高、工资.. this.info = info // 对象类型,每一项为两个数字的数组,表示可接受的最低和最高值 this.target = target // 考虑列表 this.enemyList = [] } // 注册相亲对象及家长 registEnemy(...enemy) { this.enemyList.push(...enemy) } // 检查所有相亲对象及其家长的条件 checkAllPurpose() { this.enemyList.forEach(enemy => enemy.info && this.checkPurpose(enemy)) } // 检查对方是否满足自己的要求,并发信息 checkPurpose(enemy) { // 对可枚举属性进行遍历操作,确认是否全部符合条件 const result = Object.keys(this.target).every(key => { const [low, high] = this.target[key] return low <= enemy.info[key] && enemy.info[key] <= high }) // 通知对方 enemy.receiveResult(result, this, enemy) } // 接受到对方的信息 receiveResult(result, they, me) { result ? console.log(`${they.name}:我觉得合适~ \t(我的要求 ${me.name} 已经满足)`) : console.log(`${they.name}:你是个好人! \t(我的要求 ${me.name} 不能满足!)`) } } // 男方 const ZhangXiaoShuai = new Person( '张小帅', { age: 25, height: 171, salary: 5000 }, { age: [23, 27] } ) /// 男方家长 const ZhangXiaoShuaiParent = new Person( '张小帅家长', null, { height: [160, 167] } ) // 女方 const LiXiaoMei = new Person( '李小美', { age: 23, height: 160 }, { age: [25, 27] } ) // 女方家长 const LiXiaoMeiParent = new Person( '李小美家长', null, { salary: [10000, 20000] } ) // 注册,每一个 person 实例都需要注册对方家庭成员的信息 ZhangXiaoShuai.registEnemy(LiXiaoMei, LiXiaoMeiParent) ZhangXiaoShuaiParent.registEnemy(LiXiaoMei, LiXiaoMeiParent) LiXiaoMei.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent) LiXiaoMeiParent.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent) // 检查对方是否符合要求,同样,每一个 person 实例都需要执行检查 ZhangXiaoShuai.checkAllPurpose() // 张小帅:我觉得合适~ (我的要求 李小美 已经满足) LiXiaoMei.checkAllPurpose() // 李小美:我觉得合适~ (我的要求 张小帅 已经满足) ZhangXiaoShuaiParent.checkAllPurpose() // 张小帅家长:我觉得合适~ (我的要求 李小美 已经满足) LiXiaoMeiParent.checkAllPurpose() // 李小美家长:你是个好人! (我的要求 张小帅 不能满足!)
我们还可以使用对象的形式改写,或者使用 Object.create() 赋值原型的方式将方法放在原型上,也可以使用原型继承的方式,JavaScript 的灵活性让你可以自由选择习惯的方式。
比如,使用对象的形式改写,而不使用类:
讯享网const PersonFunc = { // 注册相亲对象及家长 registEnemy(...enemy) { this.enemyList.push(...enemy) }, // 检查所有相亲对象及其家长的条件 checkAllPurpose() { this.enemyList.forEach(enemy => enemy.info && this.checkPurpose(enemy)) }, // 检查对方是否满足自己的要求,并发信息 checkPurpose(enemy) { // 对可枚举属性进行遍历操作,确认是否全部符合条件 const result = Object.keys(this.target).every(key => { const [low, high] = this.target[key] return low <= enemy.info[key] && enemy.info[key] <= high }) // 通知对方 enemy.receiveResult(result, this, enemy) }, // 接受到对方的信息 receiveResult(result, they, me) { result ? console.log(`${they.name}:我觉得合适~ \t(我的要求 ${me.name} 已经满足)`) : console.log(`${they.name}:你是个好人! \t(我的要求 ${me.name} 不能满足!)`) } } // 男方 const ZhangXiaoShuai = { ...PersonFunc, name: '张小帅', info: { age: 25, height: 171, salary: 5000 }, target: { age: [23, 27] }, enemyList: [] } // 男方家长 const ZhangXiaoShuaiParent = { ...PersonFunc, name: '张小帅家长', info: null, target: { height: [160, 167] }, enemyList: [] } // 女方 const LiXiaoMei = { ...PersonFunc, name: '李小美', info: { age: 23, height: 160 }, target: { age: [25, 27] }, enemyList: [] } // 女方家长 const LiXiaoMeiParent = { ...PersonFunc, name: '李小美家长', info: null, target: { salary: [10000, 20000] }, enemyList: [] } // 注册,每一个 person 实例都需要注册对方家庭成员的信息 ZhangXiaoShuai.registEnemy(LiXiaoMei, LiXiaoMeiParent) ZhangXiaoShuaiParent.registEnemy(LiXiaoMei, LiXiaoMeiParent) LiXiaoMei.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent) LiXiaoMeiParent.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent) // 检查对方是否符合要求,同样,每一个 person 实例都需要执行检查 ZhangXiaoShuai.checkAllPurpose() // 张小帅:我觉得合适~ (我的要求 李小美 已经满足) LiXiaoMei.checkAllPurpose() // 李小美:我觉得合适~ (我的要求 张小帅 已经满足) ZhangXiaoShuaiParent.checkAllPurpose() // 张小帅家长:我觉得合适~ (我的要求 李小美 已经满足) LiXiaoMeiParent.checkAllPurpose() // 李小美家长:你是个好人! (我的要求 张小帅 不能满足!)
这时我们可以引入媒人(中介者),专门处理对象之间的耦合关系,所有对象间相互不了解,只与媒人交互,如果引入了新的相关方,也只需要通知媒人即可。看一下实现:
// 男方 const ZhangXiaoShuai = { name: '张小帅', family: '张小帅家', info: { age: 25, height: 171, salary: 5000 }, target: { age: [23, 27] } } // 男方家长 const ZhangXiaoShuaiParent = { name: '张小帅家长', family: '张小帅家', info: null, target: { height: [160, 167] } } // 女方 const LiXiaoMei = { name: '李小美', family: '李小美家', info: { age: 23, height: 160 }, target: { age: [25, 27] } } // 女方家长 const LiXiaoMeiParent = { name: '李小美家长', family: '李小美家', info: null, target: { salary: [10000, 20000] } } // 媒人 const MatchMaker = { // 媒人的花名册 matchBook: {}, // 注册各方 registPersons(...personList) { personList.forEach(person => { // 将家长和孩子放到一起存入花名册 if (this.matchBook[person.family]) { this.matchBook[person.family].push(person) } else{ this.matchBook[person.family] = [person] } }) }, // 检查对方家庭的孩子对象是否满足要求 checkAllPurpose() { Object.keys(this.matchBook) // 遍历名册中所有家庭 .forEach((familyName, idx, matchList) =>matchList // 对于其中一个家庭,过滤出名册中其他的家庭 .filter(match => match !== familyName) // 遍历该家庭中注册到名册上的所有成员 .forEach(enemyFamily => this.matchBook[enemyFamily] .forEach(enemy => this.matchBook[familyName] // 逐项比较自己的条件和他们的要求 .forEach(person => enemy.info && this.checkPurpose(person, enemy) ) )) ) }, // 检查对方是否满足自己的要求,并发信息 checkPurpose(person, enemy) { // 对可枚举属性进行遍历操作,确认是否全部符合条件 const result = Object.keys(person.target).every(key => { const [low, high] = person.target[key] return low <= enemy.info[key] && enemy.info[key] <= high }) // 通知对方 this.receiveResult(result, person, enemy) }, // 通知对方信息 receiveResult(result, person, enemy) { result ? console.log(`${person.name} 觉得合适~ \t(${enemy.name} 已经满足要求)`) : console.log(`${person.name} 觉得不合适! \t(${enemy.name} 不能满足要求!)`) } } // 注册 MatchMaker.registPersons(ZhangXiaoShuai, ZhangXiaoShuaiParent, LiXiaoMei, LiXiaoMeiParent) MatchMaker.checkAllPurpose() // 小帅 觉得合适~(李小美 已经满足要求) // 张小帅家长 觉得合适~(李小美 已经满足要求) // 李小美 觉得合适~ (张小帅 已经满足要求) // 李小美家长 觉得不合适!(张小帅 不能满足要求!)
可以看到,除了媒人之外,其他各个角色都是独立的,相互不知道对方的存在,对象间关系被解耦,我们甚至可以方便地添加新的对象。比如赵小美家同时还在考虑着孙小拽:
讯享网// 重写上面「注册」之后的代码 // 引入孙小拽 const SunXiaoZhuai = { name: '孙小拽', familyType: '男方', info: { age: 27, height: 173, salary: 20000 }, target: { age: [23, 27] } } // 孙小拽家长 const SunXiaoZhuaiParent = { name: '孙小拽家长', familyType: '男方', info: null, target: { height: [160, 170] } } // 注册,这里只需要注册一次 MatchMaker.registPersons( ZhangXiaoShuai, ZhangXiaoShuaiParent, LiXiaoMei, LiXiaoMeiParent, SunXiaoZhuai, SunXiaoZhuaiParent ) // 检查对方是否符合要求,也只需要检查一次 MatchMaker.checkAllPurpose() // 张小帅 觉得合适~ (李小美 已经满足要求) // 张小帅家长 觉得合适~ (李小美 已经满足要求) // 孙小拽 觉得合适~ (李小美 已经满足要求) // 孙小拽家长 觉得合适~ (李小美 已经满足要求) // 李小美 觉得合适~ (张小帅 已经满足要求) // 李小美家长 觉得不合适! (张小帅 不能满足要求!) // 李小美 觉得合适~ (孙小拽 已经满足要求) // 李小美家长 觉得合适~ (孙小拽 已经满足要求)
从这个例子就已经可以看出中介者模式的优点了,因为各对象之间的相互引用关系被解耦,从而令系统的可扩展性、可维护性更好。
【总结归纳】中介者模式的通用实现:
- Colleague: 同事对象,只知道中介者而不知道其他同事对象,通过中介者来与其他同事对象通信;
- Mediator: 中介者,负责与各同事对象的通信;
9、备忘录模式
备忘录模式:在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。
【典例】缓存上一页的内容
当我们开发一个分页组件的时候,点击下一页获取新的数据,但是当点击上一页时,又重新获取数据,造成无谓的流量浪费,这时可以对数据进行缓存。
// 备忘录模式伪代码 var Page = function () { // 通过cache对象缓存数据 var cache = {} return function (page, fn) { if (cache[page]) { showPage(page, cache[page]) } else { $.post('/url', function (data) { showPage(page, data) cache[page] = data }) } fn && fn() } }
10、解释器模式
解释器模式:给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
讯享网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);
11、职责链模式
职责链模式可能在真实的业务代码中见的不多,但是作用域链、原型链、DOM 事件流的事件冒泡,都有职责链模式的影子。
职责链模式:类似多米诺骨牌, 通过请求第一个条件, 会持续执行后续的条件, 直到返回结果为止。
职责链模式的原理:
- 作用域链:查找变量时,先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象;
- 原型链:当读取实例的属性时,如果找不到,就会查找当前对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止;
- 事件冒泡: 事件在 DOM 元素上触发后,会从最内层的元素开始发生,一直向外层元素传播,直到全局 document 对象;
职责链模式的优缺点:
- 优点:
- 和命令模式类似,由于处理请求的职责节点可能是职责链上的任一节点,所以请求的发送者和接受者是解耦的;
- 通过改变链内的节点或调整节点次序,可以动态地修改责任链,符合开闭原则;
- 缺点:
- 并不能保证请求一定会被处理,有可能到最后一个节点还不能处理;
- 调试不便,调用层次会比较深,也有可能会导致循环引用;
职责链模式的适用场景:
- 需要多个对象处理同一个请求,具体该请求由哪个对象处理在运行时才确定;
- 在不明确指定接收者的情况下,向多个对象中的其中一个提交请求的话,可以使用职责链模式;
- 如果想要动态指定处理一个请求的对象集合,可以使用职责链模式;
【典例】请假
var askLeave = function (duration) { if (duration <= 0.5) { console.log('小组领导经过一番心理斗争:批准了') } else if (duration <= 1) { console.log('部门领导经过一番心理斗争:批准了') } else if (duration <= 2) { console.log('总经理经过一番心理斗争:批准了') } else { console.log('总经理:不准请这么长的假') } } askLeave(0.5) // 小组领导经过一番心理斗争:批准了 askLeave(1) // 部门领导经过一番心理斗争:批准了 askLeave(2) // 总经理经过一番心理斗争:批准了 askLeave(3) // 总经理:不准请这么长的假
上面的实现没有问题,也可以正常运行,但正常情况下,处理逻辑可能就不仅仅是一个 console.log 这么简单,而是包含一些年假、调休、项目忙碌情况的复杂判断,此时这个 askLeave 方法就变得庞大而臃肿,如果中间增加一个新的领导层,可以批准 1.5 天的假期,那么你就要修改这个庞大的 askLeave方法,维护工作变得复杂。
这里我们可以将不同领导的处理逻辑(也就是职责节点)提取出来,让不同节点的职责逻辑界限变得明显,代码结构更明显。请假的时候直接找小组领导,如果小组领导处理不好,直接把请求传递给部门领导,部门领导处理不了则传递给总经理。
讯享网// 小组领导处理逻辑 var askLeaveGroupLeader = function(duration) { if (duration <= 0.5) { console.log('小组领导经过一番心理斗争:批准了') } else{ askLeaveDepartmentLeader(duration) } } // 部门领导处理逻辑 var askLeaveDepartmentLeader = function(duration) { if (duration <= 1) { console.log('部门领导经过一番心理斗争:批准了') } else{ askLeaveGeneralLeader(duration) } } // 总经理处理逻辑 var askLeaveGeneralLeader = function(duration) { if (duration <= 2) { console.log('总经理经过一番心理斗争:批准了') } else{ console.log('总经理:不准请这么长的假') } } askLeaveGroupLeader(0.5) // 小组领导经过一番心理斗争:批准了 askLeaveGroupLeader(1) // 部门领导经过一番心理斗争:批准了 askLeaveGroupLeader(2) // 总经理经过一番心理斗争:批准了 askLeaveGroupLeader(3) // 总经理:不准请这么长的假
上面的实现,逻辑倒是清晰了,也不会有个超大的函数一把梭,但是还有个问题,比如 askLeaveGroupLeader这个函数里就直接耦合了 askLeaveDepartmentLeader这个函数,其他函数也是各自耦合在一起,如果要在其中两个职责节点中间增加一个节点,或者去掉一个节点,那么就要同时改动相邻的职责节点函数,这就违反了开闭原则,我们希望增加新的职责节点的时候,对原来的代码没有影响。
这时我们可以引入职责链模式,将职责节点的下一个节点使用拼接的方式,而不是在声明的时候就固定。使用职责链模式重构:
// 小组领导 var GroupLeader = { nextLeader: null, setNext: function(next) { this.nextLeader = next }, handle: function(duration) { if (duration <= 0.5) { console.log('小组领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } } // 部门领导 var DepartmentLeader = { nextLeader: null, setNext: function(next) { this.nextLeader = next }, handle: function(duration) { if (duration <= 1) { console.log('部门领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } } // 总经理 var GeneralLeader = { nextLeader: null, setNext: function(next) { this.nextLeader = next }, handle: function(duration) { if (duration <= 2) { console.log('总经理经过一番心理斗争:批准了') } else{ console.log('总经理:不准请这么长的假') } } } // 设置小组领导的下一个职责节点为部门领导 GroupLeader.setNext(DepartmentLeader) // 设置部门领导的下一个职责节点为总经理 DepartmentLeader.setNext(GeneralLeader) GroupLeader.handle(0.5) // 小组领导经过一番心理斗争:批准了 GroupLeader.handle(1) // 部门领导经过一番心理斗争:批准了 GroupLeader.handle(2) // 总经理经过一番心理斗争:批准了 GroupLeader.handle(3) // 总经理:不准请这么长的假
这样,将职责的链在使用的时候再拼起来,灵活性好,比如如果要在部门领导和总经理中间增加一个新的职责节点,那么在使用时:
讯享网// 新领导 var MewLeader = { nextLeader: null, setNext: function(next) { this.nextLeader = next }, handle: function(duration) {} } // 设置小组领导的下一个职责节点为部门领导 GroupLeader.setNext(DepartmentLeader) // 设置部门领导的下一个职责节点为新领导 DepartmentLeader.setNext(MewLeader) // 设置新领导的下一个职责节点为总经理 MewLeader.setNext(GeneralLeader)
但是我们看到之前的内容有很多重复代码,比如 Leader 对象里的 nextLeader、setNext 里的逻辑就是一样的,可以用继承来避免这部分重复。
首先使用 ES5 的方式:
// 领导基类 var Leader = function() { this.nextLeader = null } Leader.prototype.setNext = function(next) { this.nextLeader = next } // 小组领导 var GroupLeader = new Leader() GroupLeader.handle = function(duration) { if (duration <= 0.5) { console.log('小组领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } // 部门领导 var DepartmentLeader = new Leader() DepartmentLeader.handle = function(duration) { if (duration <= 1) { console.log('部门领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } // 总经理 var GeneralLeader = new Leader() GeneralLeader.handle = function(duration) { if (duration <= 2) { console.log('总经理经过一番心理斗争:批准了') } else{ console.log('总经理:不准请这么长的假') } } // 设置小组领导的下一个职责节点为部门领导 GroupLeader.setNext(DepartmentLeader) // 设置部门领导的下一个职责节点为总经理 DepartmentLeader.setNext(GeneralLeader) GroupLeader.handle(0.5) // 小组领导经过一番心理斗争:批准了 GroupLeader.handle(1) // 部门领导经过一番心理斗争:批准了 GroupLeader.handle(2) // 总经理经过一番心理斗争:批准了 GroupLeader.handle(3) // 总经理:不准请这么长的假
我们使用 ES6 的 Class 语法改造一下:
讯享网// 领导基类 class Leader { constructor() { this.nextLeader = null } setNext(next) { this.nextLeader = next } } // 小组领导 class GroupLeader extends Leader { handle(duration) { if (duration <= 0.5) { console.log('小组领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } } // 部门领导 class DepartmentLeader extends Leader { handle(duration) { if (duration <= 1) { console.log('部门领导经过一番心理斗争:批准了') } else{ this.nextLeader.handle(duration) } } } // 总经理 class GeneralLeader extends Leader { handle(duration) { if (duration <= 2) { console.log('总经理经过一番心理斗争:批准了') } else{ console.log('总经理:不准请这么长的假') } } } const zhangSan = new GroupLeader(); const liSi = new DepartmentLeader(); const wangWu = new GeneralLeader(); // 设置小组领导的下一个职责节点为部门领导 zhangSan.setNext(liSi) // 设置部门领导的下一个职责节点为总经理 liSi.setNext(wangWu) zhangSan.handle(0.5) // 小组领导经过一番心理斗争:批准了 zhangSan.handle(1) // 部门领导经过一番心理斗争:批准了 zhangSan.handle(2) // 总经理经过一番心理斗争:批准了 zhangSan.handle(3) // 总经理:不准请这么长的假
使用链模式重构:
// 领导基类 var Leader = function() { this.nextLeader = null } Leader.prototype.setNext = function(next) { this.nextLeader = next return next } // 小组领导 var GroupLeader = new Leader() GroupLeader.handle = function(duration) {} // 部门领导 var DepartmentLeader = new Leader() DepartmentLeader.handle = function(duration) {} // 总经理 var GeneralLeader = new Leader() GeneralLeader.handle = function(duration) {} // 组装职责链 // 设置小组领导的下一个职责节点为部门领导 // 设置部门领导的下一个职责节点为总经理 GroupLeader.setNext(DepartmentLeader).setNext(GeneralLeader)
ES6 方式处理下:
讯享网// 领导基类 class Leader { constructor() { this.nextLeader = null } setNext(next) { this.nextLeader = next return next } } // 小组领导 class GroupLeader extends Leader { handle(duration) {} } // 部门领导 class DepartmentLeader extends Leader { handle(duration) {} } // 总经理 class GeneralLeader extends Leader { handle(duration) {} } const zhangSan = new GroupLeader() const liSi = new DepartmentLeader() const wangWu = new GeneralLeader() // 组装职责链 // 设置小组领导的下一个职责节点为部门领导 // 设置部门领导的下一个职责节点为总经理 zhangSan.setNext(liSi).setNext(wangWu)
【参考文章】
JavaScript设计模式
js设计模式【详解】总目录——最常用的20种设计模式
javaScript设计模式统计 - 知乎(23种设计模式)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/32022.html