目录
前言
一、创建型模式
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 种行为型等。
原型模式(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出来的实例,拥有相同的原型上的属性和方法。
【拓展】用构造函数创建函数时不可以使用箭头函数。
单例模式(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}
可见,单例模式可以创建多个实例,但是,只要改变其中任一实例对象的属性值,其他所有的实例对象的属性值都变了。所以,单例类只能有一个实例,否则就会出错。
【拓展】为什么改变单例模式的一个实例对象的属性值,其所有的实例对象的属性值都变了呢?
工厂模式:根据不同的输入返回不同类的实例,一般用来创建同一类对象。
工厂方式的主要思想是将对象的创建与对象的实现分离。
工厂模式的使用场景:
工厂模式的优缺点:
工厂模式与模板方法模式的主要区别是:
【典例】点菜
// 饭店方法
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 这个菜本店没有
【总结归纳】工厂模式的通用实现:
【拓展】箭头函数没有函数声明提升。所以上述代码中,对于 getRandomColor 方法,如果采用箭头函数,则必须将 for 循环代码移至 getRandomColor 方法下面,否则会报错;如果采用函数声明的方式创建 getRandomColor 方法,则不会报错。
工厂模式:根据输入的不同返回不同类的实例,一般用来创建同一类对象。工厂方式的主要思想是将对象的创建与对象的实现分离。
抽象工厂模式:通过对类的工厂抽象使其业务用于对产品类簇的创建,而不是负责创建某一类产品的实例。关键在于使用抽象类制定了实例的结构,调用者直接面向实例的结构编程,从实例的具体实现中解耦。
抽象工厂模式的优缺点:
抽象工厂模式的使用场景:如果一组实例都有相同的结构,那么就可以使用抽象工厂模式。
抽象工厂模式与工厂模式的区别:
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 本店没有这个
【总结归纳】抽象工厂模式的通用实现:
// 工厂 抽象类
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,显然后者是未定义的。
参考:JavaScript 设计模式学习第十篇-建造者模式
建造者模式用于:分步构建一个复杂的对象,将一个复杂对象的 构建层与其表示层分离。若不是极其复杂的对象,应选择使用对象字面或工厂模式等方式创建对象。
实现原理:通常使用链式调用来进行建造过程,最后调用 build() 方法生成最终对象。
建造者模式的优缺点:
建造者模式的适用场景:
建造者模式 与 工厂模式 的区别:
【典例】:假定我们需要建造一个车,车这个产品是由多个部件组成,车身、引擎、轮胎。汽车制造厂一般不会自己完成每个部件的制造,而是把部件的制造交给对应的汽车零部件制造商,自己只进行装配,最后生产出整车。整车的每个部件都是一个相对独立的个体,都具有自己的生产过程,多个部件经过一系列的组装共同组成了一个完整的车。
装配汽车的代码实现如下:
// 建造者,汽车部件厂家,提供具体零部件的生产
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"
// }
这样将最终产品的创建流程使用链模式来实现,相当于将指挥者退化,指挥的过程通过链模式让用户自己实现,这样既增加了灵活性,装配过程也一目了然。如果希望扩展产品的部件,那么在建造者上增加部件实现方法,再适当修改链模式即可。
【总结归纳】建造者模式的通用实现:
下面是通用的实现。
首先使用 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 角色,只不过在链式装配过程中不再封装在具体指挥者中,而是由使用者自己确定装配过程。
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。使用组合关系代替继承关系,降低抽象和实现两个可变维度的耦合度。
桥接模式的优缺点:
桥接模式的适用场景:
【典例】演奏乐器
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()
外观模式为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。
外观模式的用途:将一些复杂操作封装起来,并创建一个简单的接口用于调用。
外观模式的适用场景:
外观模式的优缺点:
外观模式与中介者模式的区别:
【典例】假如 html 中有一个 div,很多地方都要控制它的显示隐藏,但是每次都写是比较累赘,所以我们提供一个函数来实现,代码如下:
function setBox(){
var getId = document.getElementById('isShow');
return {
show : function(){
getId.style.display = 'block';
},
hide : function(){
getId.style.display = 'none';
}
}
}
享元模式:运用共享技术来有效地支持大量细粒度对象的复用,以减少创建的对象的数量。通俗来讲,享元就是共享单元,比如现在流行的共享单车、共享充电宝等,他们的核心理念都是享元模式。
享元模式适用于以下场景:
享元模式的优缺点:
【典例一】 享元模式优化图书管理:
// 书的属性
// 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 = "文件名称:" + fileName + ",文件大小:" + fileSize +""
+ "";
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,性能优化是非常明显的。
参考:JavaScript 设计模式学习第十三篇-适配器模式
适配器模式:将一个类(对象)的接口(方法、属性)转化为用户需要的另一个接口。解决类(对象)之间接口不兼容的问题。
适配器模式的优缺点:
适配器模式的适用场景:
适配器模式、代理模式以及装饰模式的区别:
【典例一】电源适配器
在中国,使用中国插头:
// 中国插头
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);
}
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
代理模式把代理对象插入到访问者和目标对象之间,从而为访问者对目标对象的访问引入一定的间接性。正是这种间接性,给了代理对象很多操作空间,比如在调用目标对象前和调用后进行一些预操作和后操作,从而实现新的功能或者扩展目标的功能。
代理模式的优缺点:
代理模式与适配器模式的区别:
正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;
反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。常用于处理跨域请求。
虚拟代理就是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。
比如:我们在浏览一些购物商城的时候,会发现,当网络不太好的情况下,有些图片是加载不出来的,会有暂无图片的一张图片去代替它实际的图片,等网路图片加载完成之后,暂无图片就会被实际的图片代替。这就是使用的图片的懒加载。图片的懒加载也可是使用虚拟代理的模式来进行设计:
// 图片懒加载
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');
缓存代理就是可以为一些开销大的运算结果提供暂时的存储,下次运算时,如果传递进来堵塞参数跟之前一致,则可以直接返回前面存储的运算结果。
比如,前后端分离,向后端请求分页的数据的时候,每次页码改变时都需要重新请求后端数据,我们可以将页面和对应的结果进行缓存,当请求同一页的时候,就不再请求后端的接口而是从缓存中去取数据。
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);
ES6 所提供 Proxy 构造函数能够让我们轻松的使用代理模式:
const proxy = new Proxy(target, handler);
Proxy 构造函数传入两个参数:要代理的对象 和 用来定制代理行为的对象。(如果想知道 Proxy 的具体使用方法,可参考阮一峰的《 ECMAScript入门 - Proxy 》)
组合模式允许你将对象组合成树形结构来表现整体和部分的层次结构,让使用者可以以一致的方式处理组合对象以及部分对象。
组合模式的适用场景:如果对象组织呈树形结构就可以考虑使用组合模式,特别是如果操作树中对象的方法比较类似时。
组合模式的优缺点:
典型的组合模式——文件夹。
【典例】用组合模式实现文件夹
// 创建文件夹
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
可以将装饰器理解为游戏人物购买的装备,例如LOL中的英雄刚开始游戏时只有基础的攻击力和法强。但是在购买的装备后,在触发攻击和技能时,能够享受到装备带来的输出加成。我们可以理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。
参考:JavaScript设计模式(五)-装饰器模式
装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。此模式可用于将特征添加到对象中,而无需修改底层的代码。
当我们接手老代码,需要对它已有的功能做个拓展。
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()
观察者模式又叫发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
观察者模式的优缺点:
观察者模式的使用场景:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
【典例】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();
// 李四:我吃了三个汉堡
// 王二:你长得可真好看
迭代器模式:用于顺序地访问聚合对象内部的元素,又无需知道对象内部结构。使用了迭代器之后,使用者不需要关心对象的内部构造,就可以按序访问其中的每个元素。
【典例】点钞机
银行里的点钞机就是一个迭代器,放入点钞机的钞票里有不同版次的人民币,每张钞票的冠字号也不一样,但当一沓钞票被放入点钞机中,使用者并不关心这些差别,只关心钞票的数量,以及是否有假币。
var bills = ['MCK013840031', 'MCK013840032', 'MCK013840033', 'MCK013840034', 'MCK013840035'];
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
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式的优缺点:
策略模式的适用场景:
策略模式和模板方法模式的区别:
【典例】薪资
使用策略模式之前:
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
【总结归纳】策略模式的通用实现
模板方法模式:父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时,重新定义算法中的某些实现步骤。模板方法模式的关键是算法步骤的骨架和具体实现分离。
模板方法模式的优缺点:
模板方法模式的使用场景:
模板方法模式与工厂模式的区别:
模板方法模式与策略模式的区别:
【典例】泡咖啡和泡茶
泡咖啡和泡茶主要有以下不同点。
经过抽象之后,不管是泡咖啡还是泡茶,我们都能整理为下面四步:
所以,不管是冲泡还是浸泡,我们都能给它一个新的方法名称,比如说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();
【总结归纳】模板方法模式的通用实现
状态模式:当一个对象的内部状态发生改变时,会导致其行为的改变,这看起来像是改变了对象。
状态模式的优缺点:
状态模式的适用场景:
【典例】红绿灯
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());
命令模式的原理:将请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
命令模式用于:将请求封装成对象,将命令的发送者和接受者解耦。即:将调用对象(用户界面、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();
访问者模式,针对于对象结构中的元素,定义在不改变该对象的前提下访问其结构中元素的新方法。
访问者模式由 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 折
}
中介者模式:用一个中介对象来封装多个对象之间的复杂交互。中介者将对象与对象之间紧密的耦合关系变得松散,从而可以独立地改变他们。
中介者模式用于解除对象与对象之间的紧耦合关系。
中介者模式的使用场景:如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
中介者模式的优缺点:
【典例】相亲
首先我们考虑一个场景,男方和女方都有一定的条件,双方之间有要求,双方家长对对方孩子也有要求,如果达不到要求则不同意这门婚事。
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()
// 张小帅 觉得合适~ (李小美 已经满足要求)
// 张小帅家长 觉得合适~ (李小美 已经满足要求)
// 孙小拽 觉得合适~ (李小美 已经满足要求)
// 孙小拽家长 觉得合适~ (李小美 已经满足要求)
// 李小美 觉得合适~ (张小帅 已经满足要求)
// 李小美家长 觉得不合适! (张小帅 不能满足要求!)
// 李小美 觉得合适~ (孙小拽 已经满足要求)
// 李小美家长 觉得合适~ (孙小拽 已经满足要求)
从这个例子就已经可以看出中介者模式的优点了,因为各对象之间的相互引用关系被解耦,从而令系统的可扩展性、可维护性更好。
【总结归纳】中介者模式的通用实现:
备忘录模式:在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。
【典例】缓存上一页的内容
当我们开发一个分页组件的时候,点击下一页获取新的数据,但是当点击上一页时,又重新获取数据,造成无谓的流量浪费,这时可以对数据进行缓存。
// 备忘录模式伪代码
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()
}
}
解释器模式:给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
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);
职责链模式可能在真实的业务代码中见的不多,但是作用域链、原型链、DOM 事件流的事件冒泡,都有职责链模式的影子。
职责链模式:类似多米诺骨牌, 通过请求第一个条件, 会持续执行后续的条件, 直到返回结果为止。
职责链模式的原理:
职责链模式的优缺点:
职责链模式的适用场景:
【典例】请假
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种设计模式)