图解设计模式(16种)

6,665 阅读25分钟

入行几年,可能发现除了发布订阅者模式和单例模式知道些外,其他的模式只是对其名称有所耳闻,只知其名,不知其义

好像这丝毫不影响我搬砖,那么学习总结设计模式的意义在哪里?

就好比,盖了个新房,十年八年的屋顶也不会漏雨。但是,万一哪一天,有块瓦破了,需要去屋顶换瓦,那么这个时候,梯子没有,瓦片没有。看着滴滴答答的雨水,是不是莫名的心酸。这个时候,就想,有梯子有瓦片该多好。

学习设计模式,就是在晴天准备梯子和瓦片,用不到则已,万一用到了,我们就可以踩着梯子换瓦片。

那么该如何学习设计模式呢?

几年前遇到了某个问题,现在想起,可能具体代码实现是忘记了。但是解决问题的思路,大概还是能七七八八的记起来的。

所以,我认为学习设计模式的思想更为重要,具体的实现,每个设计模式可能不止一种实现方式。比如状态模式,我们可以通过修改对象状态进行状态切换,但是,也可以利用数组天然的有序性进行具体行为的组装。

本文一万三千字符,阅读比较耗时,可以在右侧点击目录进入具体的设计模式。下面开始正文...

一、定义

是软件设计过程中针对特定问题简洁优雅的解决方案。

是经过大量实际项目在很长时间中验证得到的最佳实践。

是软件开发前辈在成功和失败中总结的智慧传承。

二、比喻

听到庄周梦蝶的成语,就能能感受到庄周和蝴蝶不分你我的优美意境。

听到庖丁解牛的成语,我们会为庖丁娴熟的技能由衷的赞叹。

听到海市蜃楼的成语,就知道是一个虚无缥缈的事物。

当听到单例模式,就知道该模式指的是全局只创建一个实例。

当听到发布订阅模式,就知道是用来解决事件发布者和事件监听者之间时间解耦关系。

当听到中介者模式,就知道是用来解耦多个对象之间错综复杂的交互关系。

所以,成语之于成语背后的故事。就如,模式之于背后的解决方案。

三、使用时机

如果设计模式使用不合时宜,会出现似是而非、张冠李戴的情况。

如果能深刻的理解各个设计模式的原理,那么就相当于掌握了大用小用之辩,方可游刃有余

四、使用原则

区分可变与不可变的部分,并将变化的地方进行封装。

五、区分方式

设计模式区分不在于其实现方式,而在于其解决的问题场景。

六、按照结构分类

image.png

七、图示和简单案例说明

(一)创建型

1、原型模式

我们知道JavaScript中几乎所有的数据都是对象,即使不是,通过包装类NumberBooleanString进行转换。

而且,对象的产生可以通过new 构造函数的方式产生。那么,构造函数可以通过属性prototype的属性,为其定义属性或者方法。

通过new可以产生多个对象,每个对象都可以访问构造函数prototype上的属性和方法。

这种模式就可以称为原型模式,因为是通过构造函数创建实例对象,原型模式属于创建型模式。

可以参照如下流程图:

image.png

举个简单的例子:

var Person = function (name, hobby) {
    this.name = name;
    this.hobby = hobby;
}
Person.prototype.basicInfo = {
    'address': '北京市 海淀区',
    'school': '某某大学',
    'major': '计算机',
}

Person.prototype.sayHi = function () {
    console.log('大家好,我是' + this.name, '我的爱好是' + this.hobby);
}

var perosn1 = new Person('张三', '踢足球')
perosn1.sayHi();
console.log(perosn1.basicInfo);

var person2 = new Person('李四', '玩游戏')
person2.sayHi();
console.log(perosn1.basicInfo);

var person3 = new Person('王五', '下象棋')
person3.sayHi();
console.log(perosn1.basicInfo);

var person4 = new Person('赵六', '看电影')
person4.sayHi();
console.log(perosn1.basicInfo);

当前例子中,构造函数是Person,原型上定义了公共的基本信息basicInfo和方法sayHi。通过new的方式,创建了person1person2person3person4。每个对象都可以执行方法sayHi,也可以打印公共属性basicInfo

2、单例模式

一个环境中有且只有一个实例,并且当前环境可以访问到它。

往小了说,当前环境可以是一个函数作用域、块级作用域。往大了说可以是全局window或者global环境。

image.png

举个简单的例子:

// 定义可以表示单例模式函数
var SingleInstanceMode = function (fn) {
    var instance;
    return function () {
        return instance || (instance = fn.call(this, arguments))
    }
}
// 定义产生实例的目标函数
var targetFn = function () {
    return {
        info: '这是唯一一个实例'
    }
}
// 将目标函数传入单例模式中
var createSingleInstance = SingleInstanceMode(targetFn)

// 创建实例1
var instance1 = createSingleInstance();
// 创建实例2
var instance2 = createSingleInstance();
// 判断两实例是否相等
console.log(instance1 === instance2) // true: 表示全局只有唯一一个实例

当前例子中首先定义可表示单例模式的函数,函数中通过闭包的方式锁定变量instance,在执行创建单例的函数时,如果环境中已经存在一个实例则直接返回,否则才去进行实例的创建。这就是单例模式的一种实现方式。

3、工厂模式

工厂模式指的是,批量创建对象的时候可以避免使用new + 构造函数的方式去暴露创建对象的行为,而是通过工厂模式将创建对象的行为隐藏到工厂函数内部,不仅可以批量的生产对象,而且还可以通过传入参数改变产出产品的形态。

画个工厂简单的流程图如下:

image.png

这里我们不需要关注车间一、二、三和四具体是咋操作的,只需要关系工厂入口我们输入的参数,和工厂出口产出的产品,举个例子如下:

// 定义可以生产服装的工厂
var factory = function (type, height) {
    // 定义衬衫、短裤和皮夹克的车间
    var workshop = {
        1: function (height) {
            var obj = new Object()
            obj.name = '衬衫'
            obj.height = height;
            return obj;
        },
        2: function (height) {
            var obj = new Object()
            obj.name = '短裤'
            obj.height = height;
            return obj;
        },
        3: function (height) {
            var obj = new Object()
            obj.name = '皮夹克'
            obj.height = height;
            return obj;
        },
        4: function (height) {
            var obj = new Object()
            obj.name = '西装'
            obj.height = height;
            return obj;
        },
    }
    // 不同的车间进行不同的服装生成
    return workshop[type](height)
}
// 每个车间先生成一件衣服试试机器
var shirt1 = factory(1, '175cm')
var shorts1 = factory(2, '178cm')
var jacket1 = factory(3, '180cm')
var suit1 = factory(4, '185cm')

// 机器试着没问题,再批量生成一批衬衫
var shirt1 = factory(1, '172cm')
var shirt2 = factory(1, '173cm')
var shirt3 = factory(1, '174cm')
var shirt4 = factory(1, '175cm')
var shirt5 = factory(1, '176cm')
var shirt6 = factory(1, '177cm')
var shirt7 = factory(1, '178cm')
var shirt8 = factory(1, '179cm')

当前例子中,我们不必知道工厂内部各个车间的具体情况,只需要知道服装编号1代表衬衫2代表短裤3代表皮夹克4代表西装,然后再告诉工厂穿衣者的身高,即可批量生成出一批衬衫。

当然我们也可以根据实际需求对工厂内部进行改造,改造成为我们需要的工厂。

(二)结构型

4、装饰者模式

先举个例子作为引子,假如小帅刚造了一个手机。

image.png

小帅看着不够帅,于是加了个带有“帅”字的吊坠。

image.png

其实这个吊坠有没有,手机的功能丝毫不受影响。有了吊坠,小帅觉得会手机看起来更“帅”一点点,手机还是那个手机,人机交互窗口还是窗口触摸屏,并没有变成古老的按键方式。(人机交互的屏幕即是人和手机交互的接口)

装饰者模式以其不改变原对象,并且与原对象有着相同接口的特点,广泛应用于日常开发和主流框架的功能中。

假如我们开发了一个移动端网页,有图书搜索、小游戏、音频播放和视频播放等主要功能,初期,我们并不知道这几个功能用户的使用规律。

有一天,产品经理说,我想要各个功能用户的使用规律,并且通过echarts绘制折线图和柱状图,能加吗?

这就加......

起初:

<button id="smallGameBtn">小游戏</button>
<script>
    var enterSmallGame = function () {
        console.log('进入小游戏')
    }
    document.getElementById('smallGameBtn').onclick = enterSmallGame;
</script>

通过装饰者模式增加数据埋点之后:

<button id="smallGameBtn">小游戏</button>
<script>
     Function.prototype.after= function (afterFn) {
        var selfFn = this;
        return function () {
            var ret = selfFn.apply(this, arguments)
            afterFn.apply(this.arguments)
            return ret
        }
    }
    var enterSmallGame = function () {
        console.log('进入小游戏')
    }

    var dataLog = function () {
        console.log('数据埋点')
    }

    enterSmallGame = enterSmallGame.after(dataLog)

    document.getElementById('smallGameBtn').onclick = enterSmallGame;
</script>

定义Function.prototype.after函数,其中通过闭包的方式缓存selfFn,然后返回一个函数,该函数首先执行selfFn,再执行afterFn,这里也很清晰的可以看出两个函数的执行顺序。

在当前例子中,首先执行进入小游戏的功能,然后,再执行数据埋点的功能。

可以看出,加了数据埋点,执行函数是enterSmallGame,不加也是。同时,也未对原始函数enterSmallGame内部进行修改。

5、代理模式

先举个例子作为引子,我们的本体是计算器,每天会进行大量的计算。

image.png

我们发现也会有不少重复的计算,我们引入一个代理。

image.png

图示中,访问代理进行数据的计算,如果是重复的计算,缓存代理直接返回结果。如果是首次计算,缓存代理将其传递给本体进行计算。

当本体处于保护、缓存、虚拟或者过滤等情况下时,一个数据不适合被访问或者一个方法不能被直接调用,可以采用代理模式,先创建一个代理(本体对象或者方法的替身),作为访问者和本体之间的中介或者桥梁。

再通过代码对比只是用本体进行计算,和使用代理方式进行计算的异同。

// 本体计算乘积
var mult = function(){
    var a = 1; 
    for ( var i = 0, l = arguments.length; i < l; i++ ){ 
        a = a * arguments[i]; 
    } 
    return a; 
}; 
// 代理计算乘积
var proxyMult = (function(){ 
    var cache = {}; 
    return function(){ 
        var args = Array.prototype.join.call( arguments, ',' ); 
        if ( args in cache ){ 
            return cache[ args ]; 
        } 
        return cache[ args ] = mult.apply( this, arguments ); 
    } 
})(); 

以上是本体和代理,都可以通过传递参数计算乘积,有以下两种访问方式:

  • 本体计算乘积
console.log(mult( 1, 2, 3, 4 )); // 24

计算会得出24的乘积,如果再次计算会再次进行计算,如果数据量比较大的话,会重复计算;

  • 代理计算乘积
console.log(proxyMult( 1, 2, 3, 4 )); // 24

第一次计算会计算出24的乘积,并将其存到cache中,如cache["1,2,3,4"] = 24,第二次计算的时候,发现cache中有键为"1,2,3,4"的乘积,无需重复计算,直接返回。

6、适配器模式

先举个两实体不匹配例子:

image.png

假如这两块要契合在一起,怎么办?

咱们先给A实体造个适配器,如下:

image.png

再把A实体往右推一下:

image.png

通过适配器,咱们就把A实体和B实体结合到了一起了。

适配器模式是用来解决两个软件实体之间不兼容的问题的设计模式。可以在不改变实体内部结构的情况下,在其中一个实体外层包装一个适配器,再去将两个实体进行配合使用。

再看适配器在代码中的例子:有个实体A,需要将实体A传入实体B中,实体B返回其name对应的数据,包含名称、地址和年龄。

// 实体A
var instanceA = [{
        name: '张三',
        address: '北京',
        age: 20,
    },
    {
        name: '李四',
        address: '天津',
        age: 25,
    },
    {
        name: '王五',
        address: '河北',
        age: 30,
    }
]
// 实体B
var instanceB = function (data, name) {
    return data[name]
}
// 实体A在实体B中进行调用
console.log(instanceB(instanceA, '张三')) // undefined

这里先定义实体A作为数据,定义实体B作为调用函数,将实体A放入实体B中,我们执行可以发现返回的是undefined

此时,我们定义一个适配器。

var dataAdapter = function (arr) {
    return arr.reduce((accumulator, currentValue) => {
        accumulator[currentValue['name']] = currentValue
        return accumulator
    }, {})
}

通过适配器,将数组对象转换成name作为key{name:xxx, address:xxx, age:xxx}作为value的对象。

然后,将实体A进行适配器的处理,再塞入到实体B中。

console.log(instanceB(dataAdapter(instanceA), '张三')) // {"name": "张三", "address": "北京", "age": 20}

这样,通过适配器dataAdapter,就可以将实体A在实体B进行使用,实现了两个不同实体之间不兼容的问题。

7、组合模式

搬了一天砖后,拖着疲惫的身体回家,打开家门...

image.png

从图中可以看出,开烤箱后的组合行为是烤面包和烤肠。

进厨房后的组合行为,是开烤箱后的组合行为、煮鸡蛋和煮咖啡共同组成。

回家后的行为,是由关门、开灯、进厨房的组合行为和打开电视共同组成。

这样,就可以组成一颗行为树,但是,可以发现,蓝色区域是多个行为的组合,而非行为的自身执行,真正的执行由具体的行为动作完成。

我们先定义一个可以返回组合行为对象添加和执行的函数:

var JointCommand = function () {
    return {
        commandList: [],
        add: function (command) {
            this.commandList.push(command)
        },
        execute: function () {
            for (let i = 0, command; command = this.commandList[i++];) {
                command.execute();
            }
        }
    }
}

JointCommand执行后返回的对象表示组合对象,commandList表示命令或者行为的组合,add用来为组合命令添加命令,execute表示组合对象的执行,本质上是调用组合命令列表中的command.execute()

其中,command也可能是组合对象,执行组合对象的时候,该列表层后续的command暂时不执行。会以深度遍历的方式,先执行组合对象的列表命令。依次类推,最终的叶子对象执行完后将执行权交给父级,层层向上,最终会完成整棵树的命令执行。

下面我们先从整棵树的叶子对象开始进行分析。

(1)开烤箱后的行为

// 烤面包和烤肠
var cookieToattCommand = {
    execute: function () {
        console.log('烤面包')
    }
}
var roastSausageCommand = {
    execute: function () {
        console.log('烤香肠')
    }
}
// 开烤箱后的组合行为
afterOpenOvenCommand = JointCommand();
afterOpenOvenCommand.add(cookieToattCommand);
afterOpenOvenCommand.add(roastSausageCommand);

定义烤面包和烤肠的单个对象,包含execute方法。

再执行JointCommand返回打开烤箱组合对象。

最后通过afterOpenOvenCommand.add(cookieToattCommand)afterOpenOvenCommand.add(roastSausageCommand)的方式为打开烤箱后的组合对象添加烤面包和烤香肠命令。

(2)进入厨房后的行为

var boiledEggCommand = {
    execute: function () {
        console.log('煮鸡蛋')
    }
}
var makeCoffeeCommand = {
    execute: function () {
        console.log('煮咖啡')
    }
}
// 进入厨房后的组合行为
afterEnterKitchenCommand = JointCommand();

afterEnterKitchenCommand.add(afterOpenOvenCommand);
afterEnterKitchenCommand.add(boiledEggCommand);
afterEnterKitchenCommand.add(makeCoffeeCommand);

定义煮鸡蛋和煮咖啡的单个对象,包含execute方法。

再执行JointCommand返回进入厨房组合对象。

和打开烤箱不同的地方在于,通过afterEnterKitchenCommand.add(afterOpenOvenCommand)的方式为commandList添加的是打开烤箱后的组合对象。

最后通过afterEnterKitchenCommand.add(boiledEggCommand)afterEnterKitchenCommand.add(makeCoffeeCommand)的方式为进入厨房后的组合对象添加煮鸡蛋和煮咖啡命令。

(3)回家后的行为

var closeDoorCommand = {
    execute: function () {
        console.log('关门')
    }
}
var turnOnLightCommand = {
    execute: function () {
        console.log('开灯')
    }
}
var turnOnTvCommand = {
    execute: function () {
        console.log('打开电视')
    }
}
// 回家后的组合行为
afterGoHomeCommand = JointCommand();
afterGoHomeCommand.add(closeDoorCommand);
afterGoHomeCommand.add(turnOnLightCommand);
afterGoHomeCommand.add(afterEnterKitchenCommand);
afterGoHomeCommand.add(turnOnTvCommand);

定义关门、开灯和打开电视的单个对象,包含execute方法。

再执行JointCommand返回回家后的组合对象。

最后通过afterGoHomeCommand.add(closeDoorCommand)afterGoHomeCommand.add(turnOnLightCommand)afterGoHomeCommand.add(afterEnterKitchenCommand)afterEnterKitchenCommand.add(turnOnTvCommand)的方式为回家后的组合对象添加关门开灯进入厨房组合对象打开电视

这里需要注意,进入厨房后的组合对象已经是一棵树了,是回家后组合对象的子树。

当执行afterGoHomeCommand.execute()后的执行结果是:

image.png

深度遍历流程图如下:

image.png

小结

组合模式,在执行根组合对象、节点组合对象和叶子对象时都是execute,也就说不管从哪里开始,都可以执行execute,这让组合模式的使用变得简单。访问者几乎没有上手成本。简单得像摁个按钮,只需要知道开灯、开电视、开烤箱、咖啡机等设备的开关一样。

可以通过afterEnterKitchenCommand.execute()进行进入厨房后的组合行为执行。

可以通过afterOpenOvenCommand.execute()进行打开烤箱后的组合行为执行。

也可以通过turnOnLightCommand.execute()turnOnTvCommand.execute()进行叶子对象的执行,分别执行了开灯和打开电视的命令。

最后抽象组合模式的树形结构:

image.png

组合模式表示的是局部和整体的关系,局部和整体的关系往往是通过树来描述的。树我们知道由根、枝干和叶子构成,同样,抽象的树也是由根节点,节点和叶子节点构成。

8、享元模式

享元模式是一种用于性能优化的模式,其主要方式是通过运用共享技术来实现复杂对象总量的减少。将结构整体合理划分内部状态和外部状态,内部状态是那种不变化的,稳定的,也可以称之为享元,外部状态是那种变化的,不定的。

先举个例子:餐馆,如果使用一次性筷子,就餐次数增加一次,就需要多一双筷子。

image.png

那么,假如每天500个就餐次数,一年就是500 × 365 = 182500

用代码实现一次性筷子的创建和销毁情况:

// 筷子序列号
var serialNumber = 1;
// 筷子构造函数
var Chopsticks = function (serialNumber) {
    this.serialNumber = serialNumber;
}
// 筷子管理
var chopsticksManager = (function () {
    var outerData = []
    return {
        add: function (count) {
            for (var i = 0; i < count; i++) {
                outerData.push(new Chopsticks(serialNumber++))
            }
            return outerData;
        },
        destroy: function () {
            while (outerData.length) {
                outerData.pop();
            }
        },
    }
})()
// 一次性筷子的创建
var chopsticksArr = chopsticksManager.add(182500)
// 一次性筷子的销毁
chopsticksManager.destroy()

在当前例子中,我们定义了筷子构造函数,然后通过chopsticksManager进行筷子的管理。我们不考虑筷子分批次创建和分批次销毁的情况,我们汇总成一次进行处理。通过chopsticksArr = chopsticksManager.add(182500)的方式去创建182500双筷子,使用后,再通过chopsticksManager.destroy()的方式去销毁筷子,这个过程中我们进行了182500次筷子的创建。

为了减少一次性筷子,使用公筷,我们定义1000双公筷。

image.png

用代码实现使用公筷后,使用公筷和公筷回收的情况:

// 筷子序列号
var serialNumber = 1;
// 筷子构造函数
var Chopsticks = function (serialNumber, type) {
    this.serialNumber = serialNumber; // 公筷序列号
    this.type = type; // 这双公筷的使用状态
}
// 筷子管理
var chopsticksManager = (function () {
    var innerData = [] // 提升为内部状态的享元池
    var recycleData = [] // 公筷回收池
    return {
        // 创建公筷
        add: function (count) {
            for (var i = 0; i < count; i++) {
                innerData.push(new Chopsticks(serialNumber++))
            }
            return innerData;
        },
        // 使用公筷
        use: function (count) {
            for (let i = 0; i < count; i++) {
                var item = innerData.pop()
                item.type = 'hasUsed' // 标注为已经被使用
                recycleData.push(item);
            }
        },
        // 回收公筷
        recycle: function () {
            let recycleDataLength = recycleData.length
            for (let i = 0; i < recycleDataLength; i++) {
                var item = recycleData.pop()
                item.type = 'hasRecycled'; // 标注为已经被回收
                innerData.push(item);
            }
        },
    }
})()
// 公筷创建,公筷创建是日常客流的二倍,以防客流突然增多
var chopsticksArr = chopsticksManager.add(1000);

// 有一天客流325
chopsticksManager.use(325); // 筷子的使用
chopsticksManager.recycle(); // 筷子的回收

// 有一天客流732
chopsticksManager.use(732); // 筷子的使用
chopsticksManager.recycle(); // 筷子的回收

// 有一天客流210
chopsticksManager.use(210); // 筷子的使用
chopsticksManager.recycle(); // 筷子的回收

// 日复一日,年复一年,筷子的使用就从公筷池中使用,洗净消毒回收

在当前例子中,我们定义了筷子构造函数,然后通过chopsticksManager进行筷子的管理。通过chopsticksArr = chopsticksManager.add(1000)的方式去创建1000双公筷,通过chopsticksManager.use(325)的方式去使用,使用后再通过chopsticksManager.recycle()的方式去洗净消毒回收。

那么,在整个升级改造过程中,我们节省了超十八万双一次性筷子。

一次性筷子是没有享元的情况,使用公筷后,1000双公筷相当于1000个享元,在公筷池中,我们可以进行公筷的取出,和公筷清洗和消毒后的放回。就相当于,我们在享元池中进行享元的取出和放回。

(三)行为型

9、策略模式

策略模式指的是,定义一系列的算法,把它们一个个的封装起来,通过传递一些参数,使他们可以相互替换。

举个周末从家去咖啡馆的例子:

image.png

从家去咖啡馆,有跑步、骑行和漫步的方式。也就是说,从家到咖啡馆,有三种策略可选择。

(1)策略模式的极简实现**

通过对象的键值映射关系,定义策略和具体实现之间的关系:

var strategies = {
    A: xxx,
    B: yyy,
    C: zzz
}

其中,ABC指的策略名称,xxxyyyzzz指的是具体函数(算法)。

(2)策略模式的简单案例**

① 工具函数

当项目搭建的过程中,可以通过策略模式,封装常用的优化函数防抖和节流。

const tools = {
    throttle: function (fn, time) {
        // ...
    },
    debounce: function (fn,time) {
        // ...
    },
}

② 提示样式 vue框架下页面中的弱提示toast样式,也可以根据类型进行样式的预置,比如,先在style中定义预置的样式

<style scoped>
     /*@style:defaultActive默认状态下的样式*/
    .defaultActive {
        background-color: rgba(0, 0, 0, 0.8);
        color: #fff;
    }
    /*@style:successActive成功状态下的样式*/
    .successActive {
        background-color: #f0f9eb;
        color: #67c23a;
    }
    /*@style:infoActive信息状态下的样式*/
    .infoActive {
        background-color: #f4f4f5;
        color: #909399;
    }
    /*@style:warningActive警告状态下的样式*/
    .warningActive {
        background-color: #fdf6ec;
        color: #e6a23c;
    }
    /*@style:errorActive错误状态下的样式*/
    .errorActive {
        background-color: #fef0f0;
        color: #f56c6c;
    }
</style>

利用vue的计算属性,将传入的类型和字符串'Active'拼接组成策略,如'defaultActive'、'successActive'、'infoActive'、'warningActive'和'errorActive'

<script>
    export default {
        computed: {
            className () {
                return this.type + 'Active'
            }
        }
    };
</script>

template视图端进行"策略"和样式的关联

<template>
    <div class="toast" :class=className>
        {{msg}}
    </div>
</template>
小结

策略模式,可以利用对象的键值映射关系以及函数是一等公民的特性,以key来作为策略名称,以函数作为值定义具体算法,利用这些javascript特性可以更为简单的实现策略模式。

10、迭代器模式

迭代器模式,指的是提供一种方法顺序访问一个聚合对象或者数组中的各种元素,而又不暴露该对象的内部表示。

(1)内部迭代器

内部迭代器是自动的,将回调函数传入迭代器进行执行,访问到每一个元素都会执行传入迭代器中的回调函数。

模拟内部迭代器如下:

// 定义数组原型上的mapFn内部迭代器
Array.prototype.mapFn = function (callback) {
    let arr = this;
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        newArr[i] = callback(arr[i], i, arr)
    }
    return newArr
}
// 定义原始数组
var arr = [1, 2, 3, 4, 5];
// 定义回调函数
var callback = val => val * 2;
// 执行数组的mapFn方法调用回调函数callback
var newArr = arr.mapFn(callback);
// 打印返回值
console.log(newArr)

callback函数可以传入三个参数,第一个参数表示当前的值,第二个参数表示当前索引,第三个参数表示正在操作的数组。返回值为新数组。

当前例子中,callback指的是val => val * 2,通过数组的mapFn方法执行callback函数,返回值为新的数组newArr

在实际的使用中,Array.prototype.mapFn的内部实现是看不到的,就像我们看不到数组的操作mapforEach一样,这里如果将Array.prototype.mapFn作为黑盒子。就有如下的流程:

image.png

(2)外部迭代器

外部迭代器是非自动的,提供了next方法,需要手动的去执行next()以进行下一个元素的访问。

// 定义迭代器生成函数
function makeIterator(array) {
    var nextIndex = 0;
    return {
        next: function () {
            return nextIndex < array.length ? {
                value: array[nextIndex++],
                done: false
            } : {
                value: undefined,
                done: true
            };
        }
    };
}
// 产生迭代器
var it = makeIterator(['a', 'b']);

// 通过迭代器暴露出来的next方法,外部调用迭代器
console.log(it.next()) // { "value": "a", "done": false }
console.log(it.next()) // { "value": "b", "done": false }
console.log(it.next()) // { "value": undefined, "done": true }

makeIterator返回next方法,每一次执行都会执行下一个迭代。done是否迭代结束,value是当前迭代获取到的值。如果donetrue,对应的value就是undefined

在实际的使用中,makeIterator的内部实现是看不到的,这里如果将makeIterator作为黑盒子。就有如下的流程:

image.png

小结

目前JavaScript已经内置了多种内部迭代器,比如forEachmapfilterreduce等,内部执行回调函数function(value, index, currentArr){ xxxx }对每个访问到的元素进行处理。通过generateyield配合使用也可以产生外部迭代器,通过next()方法进行下一步的执行。

11、发布订阅者模式

发布订阅者模式,我们从一条古老的街道说起

(1)有一条古老的街道

有一条古老的街道,有一天,开了一个报社,是关于财经的。(定义一个发布者类Publish

我们叫它“财经报社”。(实例化时定义报社名称publisherName

它有一张订阅者登记表。(实例化时定义一个包含订阅者名称的登记表watcherLists

用户登记表可以记录订阅者的名字。(添加订阅者名的方法addWatcher

送报员把报纸送到每一个订阅者手里。(通知订阅者的方法notify

订阅者不订阅时,报社可以移出订阅者的名字。(移出订阅者的方法removeWatcher

报社万一哪天关门时,会清空订阅者列表。(清空订阅者的方法清空发布者列表

上面的每一句话,都代表了一个伪代码,下面具体实现一个发布者类:

class Publish {
    constructor(publisherName) {
         // 发布者名称
        this.publisherName = publisherName;
         // 订阅者列表
        this.watcherLists = []
    }
    // 添加订阅者
    addWatcher(watcher) {
        this.watcherLists.push(watcher)
    }
    // 通知订阅者
    notify() {
        const watcherLists = this.watcherLists.slice()
        for (let i = 0, l = watcherLists.length; i < l; i++) {
            watcherLists[i].update()
        }
    }
    // 移除订阅者
    removeWatcher(watcherName) {
        if (!this.watcherLists.includes(watcherName)) {
            return;
        }
        for (let i = 0; i < this.watcherLists.length; i++) {
            if (this.watcherLists[i].watcherName === watcherName) {
                this.watcherLists[i].removePublishers(this.publisherName)
                this.watcherLists.splice(i, 1)
            }
        }
    }
    // 清空订阅者列表
    clearWatchers() {
        const watcherLists = this.watcherLists.slice()
        for (let i = 0, l = watcherLists.length; i < l; i++) {
            watcherLists[i].removePublishers(this.publisherName)
        }

        this.watcherLists = []
    }
}

财经报馆开业,我们new个报馆实例。

const financialNewspaper = new Publish('财经报社')
(2)来了两个财经爱好者

有一天来了两个财经爱好者。(定义一个订阅者类Watcher

订阅者是有名称的。(实例化时定义报社名称watcherName

订阅者订阅的可能不止一家报社。(实例化时定义一个包含报社(发布者)的笔记本publishers

订阅者收到报纸后的行为。(实例时定义定义订阅者的行为(事件)fn

订阅者是通过什么样的方式接收报纸的。(定义接收报纸(发布者发布的消息)的途径,这里统一为信箱方式update

订阅者可以订阅其他报社的报纸。(添加发布者的方式addPublisher

订阅者也可以取消某家报社的报纸。(移除发布者的方式removePublishers

订阅者离开这条街道时,清空报社名称的笔记本。(清空发布者列表clearPublishers

上面的每一句话,都代表了一个伪代码,下面具体实现一个订阅者类:

class Watcher {
    constructor(watcherName, fn) {
        this.watcherName = watcherName; // 订阅者名称
        this.publishers = [] // 发布者列表
        this.fn = fn // 监听者收到消息后的反应(事件)
    }
    // 更新自身事件(行为)
    update() {
        this.fn();
    }
    // 添加发布者
    addPublisher(publisher) {
        this.publishers.push(publisher)
    }
    // 移除发布者
    removePublishers(publisherName) {
        if (!this.publishers.includes(publisherName)) {
            return;
        }
        for (let i = 0; i < this.publishers.length; i++) {
            if (this.publishers[i].publisherName === publisherName) {
                this.publishers[i].removeWatcher(this.watcherName) // 通知发布者删除订阅者
                this.publishers.splice(index, 1) // 从发布者列表中清除发布者
            }
        }
    }
    // 清空发布者列表
    clearPublishers() {
        const publishers = this.publishers.slice()
        for (let i = 0, l = publishers.length; i < l; i++) {
            publishers[i].removeWatcher(this.watcherName)
        }

        this.publishers = []
    }
}

关于订阅者,我们new两个订阅者实例。

const watcherA = new Watcher('watcherA', function () {
    console.log('喝着茶,看着报纸')
})
// 定义订阅者B
const watcherB = new Watcher('watcherB', function () {
    console.log('大清早,晨读报纸')
})

财经报刊添加了两个订阅者watcherAwatcherB

financialNewspaper.addWatcher(watcherA)
financialNewspaper.addWatcher(watcherB)
// 可以打印发布者和发布者收集的订阅者列表
console.log(financialNewspaper, financialNewspaper.watcherLists);

两个细心的订阅者把财经报刊记录在了小本本上。

watcherA.addPublisher(financialNewspaper);
watcherB.addPublisher(financialNewspaper);
// 可以打印订阅者和订阅者订阅的报刊种类
console.log(watcherA, watcherA.publishers);
console.log(watcherB, watcherB.publishers);
(3)订阅者收到报纸

第二天,送报员就把报纸投进了门口邮箱(相当于财经报刊进行了消息发布)

financialNewspaper.notify()
// watcherA和watcherB收到报纸(消息)后,就触发了他们的行为
// watcherA:'喝着茶,看着报纸'
// watcherB:'大清早,晨读报纸'
(4)财经报社又来了个订阅者

有一天财经报社来了个watcherC,也订阅了报刊。

我们再new个订阅者watcherC:

const watcherC = new Watcher('watcherC', function () {
    console.log('大晚上,熬夜看报纸')
})

报社把订阅者watcherC记录在了登记表上。

financialNewspaper.addWatcher(watcherC)

同样细心的订阅者watcherC也把财经报社记录在了小本本上。

watcherC.addPublisher(financialNewspaper);
(5)街道上又开了家体育类报社

有一天街道上又开了个体育报社。

我们先new一个体育报社。

const sportsNewspaper = new Publish('体育报社')

watcherAwatcherC也是体育爱好者,所以订阅了体育报刊。

体育报社需要登记两个订阅者的姓名。

sportsNewspaper.addWatcher(watcherA)
sportsNewspaper.addWatcher(watcherC)

这两订阅者,又各自把体育报社记录在了小本本上。

watcherA.addPublisher(sportsNewspaper);
watcherC.addPublisher(sportsNewspaper);

(6)有订阅者取消体育报刊的报纸

订阅者watcherC本来不喜欢运动,起初订阅体育报刊纯粹为了凑热闹,三天的劲头已过,他决定取消体育报刊的报纸。

watcherC.removePublishers('sportsNewspaper')
(7)有订阅者要离开这条街道

有一天,watcherA要出国留学,所以就从小本本上划掉了记录的报刊名称,并且通知报社取消报纸的订阅,第二天,送报员就没再给watherA送报纸。

watcherA.clearPublishers()

这里watcherC清掉小本本上名称的同时,也会通知到报社,体育报社和财经报社同样会在等级表上清除watcherC的名称。

clearPublishers() {
    const publishers = this.publishers.slice()
    for (let i = 0, l = publishers.length; i < l; i++) {
        publishers[i].removeWatcher(this.watcherName)
    }

    this.publishers = []
}
(8)有报社要关门

岁月如梭,多年过去啦。

随着移动互联网的兴起,纸媒受到影响,这条街道的财经报社决定关门。

financialNewspaper.clearWatchers()

第二天就不再给登记表上的订阅者送报啦,订阅者收到消息后,从小本本上划掉了财经类报刊的名字。

clearWatchers() {
    const watcherLists = this.watcherLists.slice()
    for (let i = 0, l = watcherLists.length; i < l; i++) {
        watcherLists[i].removePublishers(this.publisherName)
    }

    this.watcherLists = []
}

这里描述了发布者的产生、订阅者的产生、发布者发布消息的方式、订阅者接受消息的途径、订阅者接收到消息的行为、发布者的新增、订阅者的新增、发布者的离开和订阅者的离开等关系和逻辑。代码具体的执行结果,还需要学友自行运行验证。

小结

发布订阅者模式又叫观察者模式,它定义了对象间的一种一对多的关系。这种关系,既指一个发布者可以对应多个订阅者,又可以指一个订阅者也订阅多个发布者的消息。

12、命令模式

命令者模式,指的是执行主体可以执行某些特定事件,并且,支持队列等待、调起执行和事件撤销等行为。

假如有个五子棋的场景,黑棋是真人,白棋是电脑,黑棋先落子,可以悔棋。

我们先来定义一个执行者主体类:

class CommandSubject {
    constructor(name) {
        // 命令执行者
        this.executer = name;
        // 命令所在位置的列表
        this.posList = []
        // 演算步骤
        this.computedStep = []
    }

    // 命令执行函数
    execute(pos /*落子位置*/ ) {
        // 执行命令
        console.log(`棋子落在了[${pos[0]}, ${pos[1]}]的位置`)
        // 记录位置
        this.posList.push(pos)
    }
    // 撤回操作
    undo(step /* 撤回步数*/ ) {
        // 撤回命令
        for (let i = 0; i < step; i++) {
            // 撤回的位置
            const pos = this.posList.pop();
            console.log()
            // 撤回的操作
            console.log(`撤回[${pos[0]}, ${pos[1]}]位置的棋子`)
        }
    }
    // 执行演算步骤
    executeComputedCommand() {
        // 请自行实现吆
    }

}

我们new一个黑棋执行者。

var blackSubject = new CommandSubject('黑棋执行者');

假如黑棋执行者,和电脑共对弈 4 步。

blackSubject.execute([3, 4])
blackSubject.execute([4, 2])
blackSubject.execute([5, 3])
blackSubject.execute([5, 4])
console.log(blackSubject.posList)

此时的局势如图:

image.png

黑棋执行者觉得下错了,想悔棋 2 步。

blackSubject.undo(2)

此时的局势如图:

image.png

下棋时可能我们还会想一下接下来会走那几步,涉及到演算,假如我们演算了 3 步,那么这 3 步不是一下就落子的,而是等白棋落子后,黑棋执行者才能落子,这就是命令执行的队列问题。请自行实现吆~

如果感兴趣,也可以再new一个白棋执行者,互动下棋吆~

13、模板方法模式

模板方法模式由父类和子类构成,通过父类确定整个系统的执行流程,子类负责具体的流程实现,可以通过继承的方式重写父类的某些方法,但是不能改变功流程的执行顺序。体现了抽象与实现分离编程思想。

image.png

图中,父类控制了整个系统的执行流程,子类负责具体的流程实现。

(1)经典案例饮料冲制流程

我们知道,冲制饮料一般有以下步骤:
①把水煮沸
②用沸水冲泡饮料
③把饮料倒进杯子
④加调料
示例代码:

    // 父类:实现泡制饮料的子类功能的流程,本次功能有4个流程,如下:
    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.init = function () {
        this.boilWater();
        this.brew();
        this.pourInCup();
        this.addCondiments();
    }
    // 子类:具体实现泡制一杯茶的的流程
    var Tea = function () {}
    Tea.prototype = new Beverage();
    Tea.prototype.brew = function () {
        console.log('用水泡茶');
    }
    Tea.prototype.pourInCup = function () {
        console.log('将茶倒进杯子');
    }
    Tea.prototype.addCondiments = function () {
        console.log('加冰糖');
    }
    var tea = new Tea();
    tea.init()

  从以上例子可以看出,父类已经制定了泡制饮料的流程,并且确定了不管哪种饮料都需要把水煮沸的公共方法boilWater,至于brewpourInCupaddCondiments泡制茶、黑咖啡、牛奶和豆浆等饮料都有所不同,由子类去具体实现。

抽象的父类已经产生,接下来就是泡制茶的子类的具体实现,子类首先继承父类的泡制饮料的确定流程。其中,将水烧开继承父类,brewpourInCupaddCondiments方法由子类进行重写,至此,泡茶的流程已经完成,黑咖啡、牛奶和豆浆等饮料同理。

以上例子执行结果是:

(2)框架案例vue的主流程

vue2.0是最受欢迎的前端框架之一,以其小而美的特点,成为众多前端小伙伴的首选。使用vue的过程中,全局方法的定义、生命周期的使用、组件的封装和路由的实现等都感觉隐隐约约都被一种力量牢牢锁定,vue各个功能在使用的过程有序进行着。翻看vue源码时才发现:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// Vue类由各种initMixin、stateMixin、eventsMixin、lifecycleMixin和renderMixin的方法有序的混入各种功能
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

我们发现,Vue本质上是一个构造函数,在其new的时候,会执行内部唯一的初始化方法this._init

初始化方法在initMixin中实现:


  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // ...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    // ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

可以看出,初始化this._init方法是由如图的一些方法确定有序执行的。vue的创建过程中的初始化方法this_init就是一种模板方法模式。

小结

模板方法模式是众多设计模式之一,解决的主要业务场景是父类创建确定的子类功能或者任务的执行流程,子类继承的时候可以重写父类的某些方法。

14、职责链模式

职责链模式,指的是由拥有处理能力的职责节点对象组成一个链条,一个请求从链条的开始或者中间进入,都有机会被后续的职责节点对象处理。进入职责链的请求,会沿着后续链条被传递,直到请求被处理才会终止传递。

身在帝都,每天挤地铁去搬砖,发现月初、月中和月末地铁的票价都不一样。

查看规则才发现,乘车消费超过150元打五折,超过100元打八折,不足100元不打折。

这里通过职责链模拟一个处理订单金额的功能,该方法传入消费总金额和当前车程的常规票价。

先看流程图:

image.png

先进入总消费大于150元的流程,当前消费金额是否大于150元,返回金额,请求结束。如果不是,进入下一个总消费大于100元的流程。

进入总消费大于100元的流程,当前消费金额是否大于100元,返回金额,请求结束。如果不是,进入下一个总消费不足100元的流程。

在总消费不足100元的流程中,所有的请求都会被处理。

下面进行具体的代码实现:

// 1、定义各职责节点对象
// 定义超过150元的职责节点对象
var consumption150 = function (consumption, fare) {
    if (consumption > 150) {
        console.log(`地铁总消费大于150元,本次${fare}元的票价折扣价${fare * 0.5}`);
    } else {
        return 'nextProcess'; // 传递给下一个流程
    }
};
// 定义超过100元的职责节点对象
var consumption100 = function (consumption, fare) {
    if (consumption > 100) {
        console.log(`地铁总消费大于100元,本次${fare}元的票价折扣价${fare * 0.8}`);
    } else {
        return 'nextProcess'; // 传递给下一个流程
    }
};
// 定义不足100元的职责节点对象
var consumptionNormal = function (consumption, fare) {
    console.log(`地铁总消费不足100元,本次${fare}元的票价依然收费${fare}元`);
};

// 2、设置链条构造函数
var Chain = function (selfFn) {
    this.selfFn = selfFn; // 自身函数
    this.process = null; // 下一个职责节点
};
Chain.prototype.setNextProcess = function (process) {
    return this.process = process; // 设置当前节点对象的下一个节点对象
};
Chain.prototype.handleRequest = function () {
    // 执行自身函数,并返回执行结果
    var ret = this.selfFn.apply(this, arguments);
    // 返回nextProcess表示当前职责节点不能处理请求,此时将请求交个下一个职责节点
    if (ret === 'nextProcess') {
        return this.process && this.process.handleRequest.apply(this.process, arguments);
    }
    return ret;
};
// 创建节点,包含自身selfFn和是否成功请求的标志nextProcess
var chainconsumption150 = new Chain(consumption150);
var chainconsumption100 = new Chain(consumption100);
var chainconsumptionNormal = new Chain(consumptionNormal);

// 3、设置节点间的链条关系,形成职责链
chainconsumption150.setNextProcess(chainconsumption100);
chainconsumption100.setNextProcess(chainconsumptionNormal);
(1)职责链可以从任意节点进入

现在可以通过chainconsumption150.handleRequest(120, 8)的方式进入到职责链的第一个职责节点对象中。

也可以通过chainconsumption100.handleRequest(120, 8)的方式进入到职责链的第二个职责节点对象中。

(2)职责链可以进行扩展

假如,总消费超过300元以后,可以打四折。

①定义职能节点对象

var consumption300 = function (consumption, fare) {
    if (consumption > 300) {
        console.log(`地铁总消费大于300元,本次${fare}元的票价折扣价${fare * 0.4}`);
    } else {
        return 'nextProcess'; // 传递给下一个流程
    }
};

②创建链条节点

var chainconsumption300 = new Chain(consumption300);

③把节点塞入到链条中

chainconsumption300.setNextProcess(chainconsumption150);
小结

职责链模式,支持链条长度的扩展,也支持在链条中进入的位置。

15、中介者模式

中介者模式的作用就是解决对象与对象之间错综复杂的交互关系,增加一个中介者以后,所有相关的对象都通过中介者对象来通信,当一个对象发生改变后,只需要通知中介者对象即可。

(1)一个卖主,多个买主

假设有卖家A有一套面积:100平米,价格:20000元/平米的房子急需出售,他目前知道有三个买主在找房,对应关系如图:

image.png

通过代码实现卖家找买家:

// 卖家类
var Seller = function (name, info) {
    this.name = name;
    this.info = info;
}
// 卖家找买家的函数
Seller.prototype.match = function (buyer) {
    // 定义买家要求
    const buyerDemand = buyer.demand;
    // 获取需求数字
    const reg = /\d+/
    // 1、买家的要求
    let buyerRequestArea = buyerDemand.area.match(reg);
    buyerRequestArea = parseInt(buyerRequestArea);
    let buyerRequestprice = buyerDemand.price.match(reg);
    buyerRequestprice = parseInt(buyerRequestprice);
    // 2、卖家的条件
    let sellerOwnArea = this.info.area.match(reg);
    sellerOwnArea = parseInt(sellerOwnArea);
    let sellerOwnprice = this.info.price.match(reg);
    sellerOwnprice = parseInt(sellerOwnprice);
    return sellerOwnArea >= buyerRequestArea && sellerOwnprice <= buyerRequestprice;
}
// 买家类
var Buyer = function (name, demand) {
    this.name = name;
    this.demand = demand;
}

// 定义卖家
var sellA = new Seller('卖家A', {
    area: '100平米', // 卖家尺寸
    price: '20000元/平米' // 卖家要价
});

var buyerX = new Buyer('买家X', {
    area: '110平米', // 买家要求尺寸
    price: '10000元/平米' // 买家最高愿意支付
})
var buyerY = new Buyer('买家Y', {
    area: '120平米', // 买家要求尺寸
    price: '30000元/平米' // 买家最高愿意支付
})
var buyerZ = new Buyer('买家Z', {
    area: '99平米', // 买家要求尺寸
    price: '30000元/平米' // 买家最高愿意支付
})
// 卖家开始找买主
console.log(sellA.match(buyerX)); // true:没找到
console.log(sellA.match(buyerY)); // true:没找到
console.log(sellA.match(buyerZ)); // true:找到了

当前例子中,先定义卖家类,并为卖家定义match方法去匹配买家,如果其售卖面积大于买家要求,且售卖价格低于买家最高愿意支付,我们认为该买主是意向客户。

当前例子中刚好找到第三个买家Z的时候,就找到了。但是实际情况可能是找了几十个也没找到合适的意向客户。

(2)多个卖主,多个买主

假设又有卖主B卖主C也加入了卖方行列中,此时的对应关系如图:

image.png

如果,我们依然按照以上的方式为卖主B卖主C寻找买主,那么,此时的对应关系就已经开始复杂起来。如果有成千上万个卖主买主在进行交易的匹配,交易状况就更加复杂,一个卖主可能会和几十个买主沟通筛选,一个买主也可能和几十个卖主沟通筛选。

这个时候,就有必要通过中介者模式进行改造了,为了展示主要逻辑,以下去掉价格和平米单价的单位。

// 卖家类
var Seller = function (name, info) {
    this.name = name;
    this.info = info;
}

// 买家类
var Buyer = function (name, demand) {
    this.name = name;
    this.demand = demand;
}

// 引入中介者
var broker = (function () {
    var sellerList = [];
    var buyerList = [];
    var operations = {}
    operations.addSellers = function (seller) {
        sellerList.push(seller)
    }

    operations.addBuyers = function (buyer) {
        buyerList.push(buyer)
    }

    operations.findBuyer = function (seller) {
        const result = []
        // 遍历所有的买家
        buyerList.map(v => {
            console.log(v.demand, seller);
            if (seller.info.price <= v.demand.price && seller.info.area >= v.demand.area) {
                result.push(v);
            }
        })
        return result
    }

    operations.findSeller = function (buyer) {
        const result = []
        // 遍历所有的买家
        sellerList.map(v => {
            if (v.info.price <= buyer.demand.price && v.info.area >= buyer.demand.area) {
                result.push(v);
            }
        })
        return result;
    }

    return operations;
})()

// 定义卖家,并将其添加到中介者卖家列表中
var sellA = new Seller('卖家A', {
    area: 100, // 卖家尺寸
    price: 20000 // 卖家要价
});
var sellB = new Seller('卖家B', {
    area: 90, // 卖家尺寸
    price: 18000 // 卖家要价
});

var sellC = new Seller('卖家C', {
    area: 120, // 卖家尺寸
    price: 22000 // 卖家要价
});

broker.addSellers(sellA)
broker.addSellers(sellB)
broker.addSellers(sellC)

// 定义买家,并将其添加到中介者买家列表中
var buyerX = new Buyer('买家X', {
    area: 110, // 买家要求尺寸
    price: 10000 // 买家最高愿意支付
})
var buyerY = new Buyer('买家Y', {
    area: 80, // 买家要求尺寸
    price: 30000 // 买家最高愿意支付
})
var buyerZ = new Buyer('买家Z', {
    area: 100, // 买家要求尺寸
    price: 30000 // 买家最高愿意支付
})

broker.addBuyers(buyerX)
broker.addBuyers(buyerY)
broker.addBuyers(buyerZ)

例子中,我们除了定义卖家类和买家类,我们还引入了中介者,中介者拥有卖家信息列表,也拥有买家信息列表。当有卖家需要卖方时,可以将房屋信息和个人姓名留给中介者,中介者将其推入到卖家信息列表中。当有买家需要买房时,可以将购买需求留给中介者,中介者将其推入到买家需求列表中。

有一天,卖家A告诉中介者,他着急用钱,他的房子着急出手。于是中介者开始帮其寻找买主:

var buyers = broker.findBuyer(sellA)

有一天,买家Z告诉中介者,他现在手头有钱了,想全款买套房。于是中介者开始帮其寻找买主:

var sellers = broker.findSeller(buyerZ)
小结

我们发现,引入中介者以后,卖家和买家再也不用去为寻找买家或者卖家而烦恼,中介者拥有大量的卖主和买主信息,为其快速精准匹配。这大概也是中介这个职业兴起,并且长盛不衰的原因之一。

16、状态模式

状态模式,指的是事物内部状态的变化,会导致事物具体行为的变化。并且,状态的切换可以是循环的。最简单的例子是生活中的开关,基本都是状态模式的使用。

image.png

(1)利用实例对象的切换
// 定义灯类
var Light = function () {
    this.currState = stateManager.off; // 灯的状态
    this.button = null; // 开关
};
Light.prototype.init = function () {
    var button = document.createElement('button'),
        self = this;
    button.innerHTML = '关灯';
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.currState.changeState.call(self); // 把请求委托给 stateManager 状态机
    }
};
// 定义灯的状态管理对象
var stateManager = {
    off: {
        changeState: function () {
            this.button.innerHTML = '开灯';
            this.currState = stateManager.on;
        }
    },
    on: {
        changeState: function () {
            this.button.innerHTML = '关灯';
            this.currState = stateManager.off;
        }
    }
};
// 实例化灯
var light = new Light();
// 登初始化
light.init();

在当前例子中,首先定义灯类Light,其中有属性currState表示当前状态,button表示开关。定义的init方法会首先创建开关,再为开关绑定切换开关状态的函数changeState

定义的状态管理对象中包含属性offon的具体行为对象,每个行为的执行都是通过其中的changeState函数来实现,该函数触发时就会将当前灯的状态进行切换。

(2)利用数组的有序性
// 定义灯的类
var Light = function () {
    this.currentIndex = 0; // 设置初始索引
    this.button = null; // 开关
};
Light.prototype.init = function () {
    var button = document.createElement('button'),
        self = this;
    button.innerHTML = '关灯';
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        excuteStateFn(self);
    }
};
// 定义状态状态切换列表
var stateList = [
    function changeState(light) {
        light.button.innerHTML = '开灯';
    },
    function changeState(light) {
        light.button.innerHTML = '关灯';
    }
]
// 定义状态切换执行函数
function excuteStateFn(light) {
    light.currentIndex >= stateList.length && (light.currentIndex = 0); // 进行边界状态的控制
    stateList[light.currentIndex++](light) // 切换状态

}
// 实例化灯
var light = new Light();
// 灯进行初始化
light.init();

在当前例子中,首先定义灯类Light,其中有属性currentIndex表示行为对应的索引,button表示开关。定义的init方法会首先创建开关,再为开关绑定切换开关状态的函数excuteStateFn(self)

定义的状态切换列表中stateList包含数组元素offon的具体行为函数。

再定义行为切换执行函数excuteStateFn,每个行为的执行都是通过执行当前索引对应的行为函数stateList[light.currentIndex++](light)来实现的,通过修改当前索引的值来切换下一次执行的状态索引。

小结

状态模式的实现不止一种实现思路,可以利用行为函数执行时修改当前实例对象的状态实现,也可以利用数组天然的有序性通过索引的改变指向对应的执行函数,当然,还可能有其他实现方式。只要遵循状态模式状态的切换可以是循环的,任何实现都是正确的。

总结

以上简单介绍了十六种设计模式,当然除此之外,还有其他的设计模式。也许,不远的将来,会有新的,被众多人所承认的设计模式产生。

通过以上介绍我们可以先略微掌握设计模式的,不断修道,终会将设计模式内化,在合适的场景,可能会起到事半功倍的效果,扳子、钳子、改锥、电钻我们都有,万一哪天要打个孔,上个螺丝,就变得非常简单。

学习设计模式也是,在某些错综复杂的场景中,拿出一两个设计模式思想,高效处理业务难点,就如打个孔,上个螺丝那么简单。

参考资料

《JavaScript》设计模式与开发实践

写在最后

2023年,新的一年,新的征程,希望各位学友狡兔三窟,即使被裁也能很快找到下一个,一起加油。

2023年,希望自己能够有高质量的文章输出,持续分享。

文中纰漏在所难免,如有纰漏请贵手留言

本文如有帮助,点赞就是对我最大的肯定和支持吆❤