Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章 | https://0x9.me/KMrv3 |
开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式。本文意在梳理常见设计模式的特点,从而对它们有比较清晰的认知。
JavaScript 中常见设计模式
各设计模式关键词
看完了上述设计模式后,把它们的关键词特点罗列出来,以后提到某种设计模式,进而联想相应的关键词和例子,从而心中有数。
设计模式 | 特点 | 案例 |
---|---|---|
单例模式 | 一个类只能构造出唯一实例 | 创建菜单对象 |
策略模式 | 根据不同参数可以命中不同的策略 | 动画库里的算法函数 |
代理模式 | 代理对象和本体对象具有一致的接口 | 图片预加载 |
迭代器模式 | 能获取聚合对象的顺序和元素 | each([1, 2, 3], cb) |
发布-订阅模式 | PubSub | 瀑布流库 |
命令模式 | 不同对象间约定好相应的接口 | 按钮和命令的分离 |
组合模式 | 组合模式在对象间形成一致对待的树形结构 | 扫描文件夹 |
模板方法模式 | 父类中定好执行顺序 | 咖啡和茶 |
享元模式 | 减少创建实例的个数 | 男女模具试装 |
职责链模式 | 通过请求第一个条件,会持续执行后续的条件,直到返回结果为止 | if else 优化 |
中介者模式 | 对象和对象之间借助第三方中介者进行通信 | 测试结束告知结果 |
装饰者模式 | 动态地给函数赋能 | 天冷了穿衣服,热了脱衣服 |
状态模式 | 每个状态建立一个类,状态改变会产生不同行为 | 电灯换挡 |
适配者模式 | 一种数据结构改成另一种数据结构 | 枚举值接口变更 |
1.单例模式
两个条件
- 确保只有一个实例
- 可以全局访问
适用
适用于弹框的实现,全局缓存
实现单例模式
1 | const singleton = function(name) { |
JavaScript 中的单例模式
因为 JavaScript 是无类的语言,而且 JS 中的全局对象符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用,
1 | var obj = {} |
弹框层的实践
实现弹框的一种做法是先创建好弹框,然后使之隐藏,这样子的话会浪费部分不必要的 DOM 开销,我们可以在需要弹框的时候再进行创建,同时结合单例模式实现只有一个实例,从而节省部分 DOM 开销。下列为登入框部分代码:
1 | const createLoginLayer = function() { |
使单例模式和创建弹框代码解耦
1 | const getSingle = function(fn) { |
1 | const createSingleLoginLayer = getSingle(createLoginLayer) |
2.策略模式
定义:根据不同参数可以命中不同的策略
JavaScript 中的策略模式
观察如下获取年终奖的 demo,根据不同的参数(level)获得不同策略方法(规则),这是策略模式在 JS 比较经典的运用之一。
1 | const strategy = { |
在函数是一等公民的 JS 中,策略模式的使用常常隐藏在高阶函数中,稍微变换下上述 demo 的形式如下,可以发现我们平时已经在使用它了,恭喜我们又掌握了一种设计模式。
1 | const S = function(salary) { |
优点
- 能减少大量的 if 语句
- 复用性好
3.代理模式
情景:小明追女生 A
- 非代理模式:小明 =花=> 女生A
- 代理模式:小明 =花=> 让女生A的好友B帮忙 =花=> 女生A
代理模式的特点
- 代理对象和本体对象具有一致的接口,对使用者友好
代理模式的种类有很多,在 JS 中最常用的为虚拟代理和缓存代理。
虚拟代理实现图片预加载
下面这段代码运用代理模式来实现图片预加载,可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离,并且在未来如果不需要预加载,只要改成请求本体代替请求代理对象就行。
1 | const myImage = (function() { |
缓存代理实现乘积计算
1 | const mult = function() { |
小 tip
在开发时候不要先去猜测是否需要使用代理模式,如果发现直接使用某个对象不方便时,再来优化不迟。
4.迭代器模式
定义:能访问到聚合对象的顺序与元素
实现一个内部迭代器
1 | function each(arr, fn) { |
可以看出内部迭代器在调用的时候非常简单,使用者不用关心迭代器内部实现的细节,但这也是内部迭代器的缺点。比如要比较两数组是否相等,只能在其回调函数中作文章了,代码如下:
1 | const compare = function(arr1, arr2) { |
实现一个外部迭代器
相较于内部迭代器,外部迭代器将遍历的权利转移到外部,因此在调用的时候拥有了更多的自由性,不过缺点是调用方式较复杂。
1 | const iterator = function(arr) { |
5.发布订阅模式
事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦,甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。
优点:在异步编程中实现更深的解耦
缺点:如果过多的使用发布订阅模式,会增加维护的难度
实现一个发布订阅模式
1 | var Event = function() { |
订阅函数逻辑一定要优先于发布函数吗
考虑以下场景:
1 | $.ajax('', () => { |
我们需要实现这样的逻辑:
1 | var ev = new Event() |
目标明确后,来着手实现它:
1 | var Event = function() { |
以上代码实现思路就是把原本在 emit
里触发的函数存到 cacheList
,再转交到 on
中触发。从而实现了发布函数先于订阅函数执行。
6.命令模式
命令模式与策略模式有些类似,在 JavaScript 中它们都是隐式的。
重要性:较低
JavaScript 中的命令模式
命令模式在 JavaScript 中也比较简单,下面代码中对按钮和命令进行了抽离,因此可以复杂项目中可以使用命令模式将界面的代码和功能的代码交付给不同的人去写。
1 | const setCommand = function(button, command) { |
7.组合模式
- 组合模式在对象间形成树形结构;
- 组合模式中基本对象和组合对象被一致对待;
- 无须关心对象有多少层,调用时只需在根部进行调用;
demo1 —— 宏命令
想象我们现在手上有个万能遥控器,当我们回家,按一下开关,下列事情将被执行:
- 煮咖啡
- 打开电视、打开音响
- 打开空调、打开电脑
我们把任务划分为 3 类,效果图如下:
接着看看结合了命令模式和组合模式的具体实现:
1 | const MacroCommand = function() { |
可以看出在组合模式中基本对象和组合对象被一致对待,所以要保证基本对象(叶对象)和组合对象具有一致方法。
demo2 —— 扫描文件夹
扫描文件夹时,文件夹下面可以为另一个文件夹也可以为文件,我们希望统一对待这些文件夹和文件,这种情形适合使用组合模式。
1 | const Folder = function(folder) { |
8.模板方法模式
定义:在继承的基础上,在父类中定义好执行的算法。
泡茶和泡咖啡
来对比下泡茶和泡咖啡过程中的异同
步骤 | 泡茶 | 泡咖啡 |
---|---|---|
1 | 烧开水 | 烧开水 |
2 | 浸泡茶叶 | 冲泡咖啡 |
3 | 倒入杯子 | 倒入杯子 |
4 | 加柠檬 | 加糖 |
可以清晰地看出仅仅在步骤 2 和 4 上有细微的差别,下面着手实现:
1 | const Drinks = function() {} |
钩子
假如客人不想加佐料(糖、柠檬)怎么办,这时可以引人钩子来实现之,实现逻辑如下:
1 | // ... |
9.享元模式
享元模式是一种优化程序性能的模式,本质为减少对象创建的个数。
以下情况可以使用享元模式:
- 有大量相似的对象,占用了大量内存
- 对象中大部分状态可以抽离为外部状态
demo
某商家有 50 种男款内衣和 50 种款女款内衣,要展示它们
方案一:造 50 个塑料男模和 50 个塑料女模,让他们穿上展示,代码如下:
1 | const Model = function(gender, underwear) { |
方案二:造 1 个塑料男模特 1 个塑料女模特,分别试穿 50 款内衣
1 | const Model = function(gender) { |
对比发现:方案一创建了 100 个对象,方案二只创建了 2 个对象,在该 demo 中,gender(性别) 是内部对象,underwear(穿着) 是外部对象。
当然在方案二的 demo 中,还可以进一步改善:
- 一开始就通过构造函数显示地创建实例,可用工场模式将其升级成可控生成
- 在实例上手动添加 underwear 不是很优雅,可以在外部单独在写个 manager 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46const Model = function(gender) {
this.gender = gender
}
Model.prototype.takephoto = function() {
console.log(`${this.gender}穿着${this.underwear}`)
}
const modelFactory = (function() { // 优化第一点
const modelGender = {}
return {
createModel: function(gender) {
if (modelGender[gender]) {
return modelGender[gender]
}
return modelGender[gender] = new Model(gender)
}
}
}())
const modelManager = (function() {
const modelObj = {}
return {
add: function(gender, i) {
modelObj[i] = {
underwear: `第${i}款衣服`
}
return modelFactory.createModel(gender)
},
copy: function(model, i) { // 优化第二点
model.underwear = modelObj[i].underwear
}
}
}())
for (let i = 1; i < 51; i++) {
const maleModel = modelManager.add('male', i)
modelManager.copy(maleModel, i)
maleModel.takephoto()
}
for (let i = 1; i < 51; i++) {
const femaleModel = modelManager.add('female', i)
modelManager.copy(femaleModel, i)
femaleModel.takephoto()
}
10.职责链模式
职责链模式:类似多米诺骨牌,通过请求第一个条件,会持续执行后续的条件,直到返回结果为止。
重要性:4 星,在项目中能对 if-else 语句进行优化
场景 demo
场景:某电商针对已付过定金的用户有优惠政策,在正式购买后,已经支付过 500 元定金的用户会收到 100 元的优惠券,200 元定金的用户可以收到 50 元优惠券,没有支付过定金的用户只能正常购买。
1 | // orderType: 表示订单类型,1:500 元定金用户;2:200 元定金用户;3:普通购买用户 |
下面用职责链模式改造代码:
1 | const order500 = function(orderType, pay, stock) { |
改造后可以发现代码相对清晰了,但是链路代码和业务代码依然耦合在一起,进一步优化:
1 | // 业务代码 |
重构后,链路代码和业务代码彻底地分离。假如未来需要新增 order300,那只需新增与其相关的函数而不必改动原有业务代码。
另外结合 AOP 还能简化上述链路代码:
1 | // 业务代码 |
职责链模式比较重要,项目中能用到它的地方会有很多,用上它能解耦 1 个请求对象和 n 个目标对象的关系。
11.中介者模式
中介者模式:对象和对象之间借助第三方中介者进行通信。
场景 demo
一场测试结束后,公布结果:告知解答出题目的人挑战成功,否则挑战失败。
1 | const player = function(name) { |
在这段代码中 A、B、C 之间没有直接发生关系,而是通过另外的 playerMiddle 对象建立链接,姑且将之当成是中介者模式了。
12.装饰者模式
装饰器模式:动态地给函数赋能。
JavaScript 的装饰者模式
生活中的例子:天气冷了,就添加衣服来保暖;天气热了,就将外套脱下;这个例子很形象地含盖了装饰器的神韵,随着天气的冷暖变化,衣服可以动态的穿上脱下。
1 | let wear = function() { |
这种方式有以下缺点:1:临时变量会变得越来越多;2:this 指向有时会出错
AOP 装饰函数
1 | // 前置代码 |
用后置代码来实验下上面穿衣服的 demo,
1 | const wear1 = function() { |
但这样子有时会污染原生函数,可以做点通变
1 | const after = function(fn, afterFn) { |
13.状态模式
状态模式:将事物内部的每个状态分别封装成类,内部状态改变会产生不同行为。
优点:用对象代替字符串记录当前状态,状态易维护
缺点:需编写大量状态类对象
场景 demo
某某牌电灯,按一下按钮打开弱光,按两下按钮打开强光,按三下按钮关闭灯光。
1 | // 将状态封装成不同类 |
非面向对象实现的状态模式
借助于 JavaScript 的委托机制,可以像如下实现状态模式:
1 | const obj = { |
14.适配者模式
适配者模式:主要用于解决两个接口之间不匹配的问题。
demo
1 | // 老接口 |
原文地址 JavaScript 中常见设计模式整理