>[info]参考书籍:《JavaScript 设计模式与开发实践》-曾探
>[warning]设计模式是不分语言的,这里只记录书中关于 JavaScript 实现某些设计模式的思路及代码,显然实现方案不是唯一的
[TOC]
# 前置知识
<span style="font-family: 楷体; font-weight: bold; font-size: 18px;">动态类型语言和鸭子类型</span>
编程语言按照数据类型大体可以分为两类:一类是 **静态类型语言**,一类是 **动态类型语言**
静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值后,才会具有某种类型。
- 静态类型语言的优点是在编译时就能发现类型不匹配的错误,编译器可帮助我们提前避免程序在运行期间有可能发生的一些错误,另外,因为程序中明确地规定了数据类型,编译器还可以针对这些信息进行一些优化工作,提高程序执行速度。
其缺点主要是迫使程序员依照强契约来编写程序,类型的声明会增加更多的代码。
- 动态类型语言的优点是编写的代码量更少,看起来更简洁,程序员可以把更多的精力放在业务逻辑上;缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。
在 JavaScript 中,当我们对一个变量赋值时,显然不需要考虑它的类型,因此 JavaScript 是一门典型的动态类型语言。(TypeScript 的出现就是为了确定变量类型,从而对一些运行时潜在的 bug 给予提示 )
*****
<span style="font-family: 楷体; font-weight: bold; font-size: 18px;">鸭子类型(duck typing)</span>
鸭子类型的通俗说法是“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子”
在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。例如,一个对象若有 push 和 pop 方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用。一个对象如果有 length 属性,也可以按照下标来存取属性且拥有 slice 和 splice 等方法,这个对象就可以被当作数组来使用。这也称为“面向接口编程,而不是面向实现编程”
*****
<span style="font-family: 楷体; font-weight: bold; font-size: 18px;">多态(polymorphism)</span>
“多态”一词源自希腊文 polymorphism,拆开来看是 poly(负数)+ morph(多态) + ism,从字面上看可以理解为负数形态。
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
例如下面这个例子:
```js
let makeSound = function (animal) {
if (animal instanceof Duck) {
console.log('嘎嘎嘎')
} else if (animal instanceof Chicken) {
console.log('咯咯咯')
}
}
let Duck = function () {}
let Chicken = function () {}
makeSound(new Duck()) // 嘎嘎嘎
makeSound(new Chicken()) // 咯咯咯
```
当我们分别向鸭和鸡发出“叫唤”的消息时,它们根据此消息作出了各自不同的反应。但这样的多态性是无法令人满意的,如果后来又增加了一只动物,比如狗,那么我们必须修改 makeSound 函数才能让狗也发出叫声,而修改越多的代码,程序出错的可能性就越大,而且当动物种类越来越大,makeSound 函数也会越来越大。
<br/>
对象的多态性提示我们,“做什么” 和 “怎么去做” 是可以分开的。
# 单例模式
单例模式的定义是:**保证一个类仅有一个实例,并提供一个访问它的全局访问点**。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的 window 对象等。
考虑一个创建悬浮框的场景,我们希望点击某个按钮时创建一个悬浮框且这个悬浮框在页面中总是唯一的,如登录窗口。
第一种解决方案是在页面加载完成好的时候便创建这个 div 浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮时它才显示;显然这种方式在用户不需要登录的情况下会白白浪费一些 DOM 节点。
第二种思路是用户点击登录按钮的时候才开始创建浮窗,每当我们点击登录按钮的时候,都会创建一个新的登录浮窗 div,可以通过点击浮窗上的关闭按钮来删除这个浮窗。为了防止频繁地创建和删除,可以通过一个变量来判断是否已经创建过浮窗。
```html
<html>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
let createLoginLayer = (function () {
let div
return function () {
if (!div) { // 首次判断 div 为 undefined
div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
div.style.display = 'none'
document.body.appendChild(div)
}
return div
}
})()
document.getElementById('loginBtn').onclick = function () {
let loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
</script>
</html>
```
上面的例子完成了一个可用的 **惰性单例**,惰性单例即在需要的时候才创建对象实例。
这个例子仍然有以下一些问题:
- 这段代码违反单一职责原则,创建对象和管理单例的逻辑都放在 createLoginLayer 对象内部
- 如果我们下一次需要创建页面中唯一的 iframe,或者 script 标签,用来跨域请求数据,就必须把 createLoginLayer 函数几乎照抄一遍
现在我们把管理单例的逻辑从原来的代码中抽离,这些逻辑被封装在 getSingle 函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数:
```
// 传入创建对象的方法 fn
let getSingle = function (fn) {
let result
return function (...args) {
return result || ( result = fn.apply(this, args) )
}
}
let createLoginLayer = function () {
let div = document.createElement('div')
div.innerHTML = '我是登录浮窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
let createSingleLoginLayer = getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function () {
let loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
```
# 策略模式
策略模式的定义:**定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换**
一个基于策略模式的程序至少由两部分组成
- 策略类,策略类封装了具体的算法,并负责具体的计算过程
- 环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类,要做到这一点,Context 中需要维护对某个策略对象的引用
考虑一个计算年终奖的例子,年终奖一般是根据员工的工资基数和年底绩效情况来发放的,例如,绩效为 S 的人年终奖有 4 倍工资;绩效为 A 的人年终奖有 3 倍工资;假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖
<br/>
最初的代码实现,我们可以编写一个名为 calculateBonus 的函数来计算每个人的奖金,这个函数需要接收两个参数:员工的工资数额和他的绩效考核等级,代码如下:
```js
let calculateBonus = function (performanceLevel, salary) {
if (performanceLevel === 'S') {
return salary * 4
}
if (performanceLevel === 'A') {
return salary * 3
}
if (performance === 'B') {
return salary * 2
}
}
calculateBonus('B', 20000) // 40000
calculateBonus('S', 6000) // 24000
```
这段代码存在着如下的显而易见的缺点:
- calculateBonues 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑分支
- calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金系数改为 5,那么我们必须深入该函数的内部实现,这是违反开放-封闭原则的
- 算法的复用性差,如果程序的其他地方需要重用这些计算奖金的算法,就只能复制粘贴。
我们可以使用策略模式重构这段代码:
```
// 定义策略类封装一系列算法
let strategies = {
"S": function (salary) {
return salary * 4
},
"A": function (salary) {
return salary * 3
},
"B": function (salary) {
return salary * 2
}
}
let calculateBonus = function (level, salary) {
return strategies[level](salary) // Context 总是把请求委托给策略对象中的某一个进行计算
}
console.log(calculateBonus('S', 20000)) // 80000
console.log(calculateBonus('A', 10000)) // 30000
```
# 代理模式
**代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。**
保护代理和虚拟代理:以一个小明请代理 B 帮忙送花给美眉 A 的例子来说明:
- 保护代理:代理 B 可以帮助 A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B 处被拒绝掉。
- 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。比如送花很贵,代理 B 会选择在 A 心情好的时候再帮忙送花
## 虚拟代理合并 HTTP 请求
假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同步到另一台备用服务器上。如果用户在短时间内点击了多次,那么就会有频繁的网络请求。
解决方案是:通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒后才把这 2 秒内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大的副作用,却能大大减轻服务器的压力。
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
<script>
let synchronousFile = function (id) {
console.log('开始同步文件, id为: ' + id)
}
let proxySynchronousFile = (function () {
let cache = [], // 保存一段时间内需要同步的 ID
timer // 定时器
return function (id) {
cache.push(id)
if (timer) { // 保证不会覆盖已经启动的定时器
return
}
timer = setTimeout(function () {
synchronousFile(cache.join(',')) // 2s 发送需要同步的 ID 集合
clearTimeout(timer)
timer = null
cache.length = 0 // 清空 ID 集合
}, 2000)
}
})()
let checkbox = document.getElementsByTagName('input')
for (let i = 0, c; c = checkbox[i++]; ) {
c.onclick = function () {
if (this.checked === true) {
proxySynchronousFile(this.id)
}
}
}
</script>
</body>
</html>
```
![](https://box.kancloud.cn/689883d44177137b81b23f603685258a_1187x716.png)
## 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果。
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为加法、乘法、减法等创建缓存代理,代码如下
```
// 计算乘积
const mult = function (...args) {
let a = 1
for (let i = 0, l = args.length; i < l; i++) {
a = a * args[i]
}
return a
}
// 计算加和
const plus = function (...args) {
let a = 0
for (let i = 0, l = args.length; i < l; i++) {
a = a + args[i]
}
return a
}
// 创建缓存代理的工厂
const createProxyFactory = function (fn) {
let cache = {}
return function (...args) {
let joinArgs = args.join(',')
if (joinArgs in cache) {
console.log('get result from cache')
return cache[joinArgs]
}
return cache[joinArgs] = fn.apply(this, args)
}
}
const proxyMult = createProxyFactory(mult),
proxyPlus = createProxyFactory(plus)
console.log(proxyMult(1, 2, 3, 4))
console.log(proxyMult(1, 2, 3, 4)) // 第二次会直接从缓存获取
console.log(proxyPlus(1, 2, 3, 4))
console.log(proxyPlus(1, 2, 3, 4))
```
- mult 等函数可以专注于自身的责任,如计算乘积、加和,缓存的功能由代理对象实现
# 观察者模式与发布-订阅模式
观察者模式确实很有用,但是在 JavaScript 中,通常我们使用一种叫做发布/订阅模式的变体来实现观察者模式。这两种模式很相似,但是也有一些值得注意的不同。
- 观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件。
- 发布/订阅模式使用一个主题/事件频道,这个频道处于想要获取通知的订阅者和发起事件的发布者之间。这个事件系统允许代码定义应用相关的事件,这个事件可以传递特殊的参数,参数中包含有订阅者所需要的值。这种想法是为了避免订阅者和发布者之间的依赖性。
- 这种和观察者模式之间的不同,使订阅者可以实现一个合适的事件处理函数,用于注册和接受由发布者广播的相关通知。
![](https://box.kancloud.cn/5c55050338fca0dc305967ab1a09387f_512x406.png)
[模拟实现 node 中的 Events 模块](https://www.jb51.net/article/159753.htm)
简单来说,实现一个发布-订阅模式有以下三步:
1. 首先指定好谁充当发布者
2. 然后给发布者添加一个**缓存列表**,用于存放回调函数以便通知订阅者
3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数;另外这些回调函数也可以接收一些参数
完整实现,可以通过上面的链接查看具体分析过程
```js
class EventEmitter {
constructor () {
this.events = {} // 事件监听函数保存的地方
}
on (eventName, listener) {
if (this.events[eventName]) {
this.events[eventName].push(listener)
} else {
// 如果没有保存过,将回调函数保存为数组
this.events[eventName] = [listener]
}
}
// 从指定名字的监听器数组中移除指定的 listener
off (eventName, listner) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(l => l !== listner)
// filter: 返回执行函数后为 true 的项所组成的数组
}
}
// 添加的监听器只执行一次,执行一次后就被销毁
once (eventName, listener) {
// 重构这个回调函数,使其执行之后可以被销毁
let reListener = (...rest) => {
listener.apply(this, rest)
this.off(eventName, reListener) // this 指向是否会有问题?
}
this.on(eventName, reListener)
}
// 可传递参数以支持函数
emit (eventName, ...rest) {
// emit 触发事件,把回调函数拉出来执行
// && 运算符:左操作数为假值则直接返回 false
this.events[eventName] && this.events[eventName].forEach(listener => listener.apply(this, rest))
}
}
```
- 这里的 EventEmitter 类就相当于上图中的 Event Channel,其提供给 Publisher emit 方法,从而触发某个事件的所有回调函数(相当于通知订阅了该事件的所有订阅者)
- 推模型与拉模型:推模型是指在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者;拉模型是发布者仅仅通知订阅者事件发生了,此外发布者要提供一些公开的接口供订阅者来主动拉取数据,这会增加代码量和复杂度。显然 JavaScript 中一般会选择使用推模型。
# 享元模式
享元(flyweight)模式是一种用于性能优化的模式,"fly" 在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
享元模式的目标是尽量减少共享对象的数量,其要求将对象的属性划分为内部状态与外部状态(这里的状态通常指属性),关于如何划分内部状态与外部状态,有以下一些经验
- 内部状态存储于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会变化
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
这样一来,我们便可以把所有内部状态相同的对象指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
外部状态在必要时被传入共享对象来组装成一个完整的对象,虽然组装一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量。因此,享元模式是一种以时间换空间的优化模式。
书中提供了一个文件上传的例子,限于篇幅这里只描述下如何利用享元模式。微云的文件上传功能可以选择依照队列,一个一个地排队上传,也支持同时选择 2000 个文件,每一个文件都对应着 JavaScript 上传对象的创建。实现这个需求可以同时 new 2000 个 upload 对象,这往往会造成浏览器卡死;使用享元模式重构可以优化性能,即使同时上传 2000个 文件,需要创建的 upload 对象数量依然是 2。
## 对象池
前端开发中,对象池使用的最多的场景大概就是跟 DOM 有关的操作。很多空间和实践都消耗在了 DOM 节点上,如何避免频繁地创建和删除 DOM 节点就成了一个有意义的话题。
假设我们在开发一个地图应用,地图上经常会出现一些标志地名的小气泡,我们称之为 toolTip;假设我们第一次搜索出现了 2 个小气泡,第二次搜索出现了 6 个小气泡;按照对象池的思想,我们并不会把第一次创建的 2 个小气泡删除掉,而是把它们放进对象池,这样在第二次搜索的结果中,我们只需要再创建 4 个小气泡而不是 6 个。
```
const toolTipFactory = (function () {
const toolTipPool = [] // toolTip对象池
// 返回一个对象,对外暴露方法
return {
create: function () {
if (toolTipPool.length === 0) {
let div = document.createElement('div') // 创建一个 dom
document.body.appendChild('div')
return div
} else {
return toolTipPool.shift() // 如果对象池不为空,则从对象池中取出一个 dom
}
},
recover: function (toolTipDom) {
return toolTipPool.push(toolTilDom) // 对象池回收 Dom
}
}
})()
// 第一次搜索:创建 2 个小气泡节点,为了方便回收,用一个数组 arr 来记录它们
const ary = []
for (let i = 0, str; str = ['A', 'B'][i++];) {
let toolTip = toolTipFactory.create()
toolTip.innerHTML = str
ary.push(toolTip)
}
// 第二次搜索:回收之前的两个小气泡并创建 6 个小气泡
for (let i = 0, toolTip; toolTip = ary[i++];) {
toolTipFactory.recover(toolTip)
}
for (let i = 0, str; str = ['A', 'B', 'C', 'D', 'E', 'F'][i++];) {
let toolTip = toolTipFactory.create()
toolTip.innerHTML = str
}
```
# 职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
下面是一个订单购买模式的例子
```
/**
*
* @param {Number} orderType 表示订单类型,1:500元定金用户 2:200元用户 3:普通用户
* @param {Boolean} pay 表示用户是否已经支付定金,true:已支付 false:未支付
* @param {Number} stock 表示当前用于普通购买的手机库存数量,已经支付过500元或者200元定金的用户不受此限制
*/
let order = function (orderType, pay, stock) {
if (orderType === 1) { // 500元定金购买模式
if (pay === true) { // 已支付定金
console.log('500元定金预购,得到100优惠券')
} else { // 未支付定金,降到普通购买模式
if (stock > 0) { // 用于普通购买的手机还有库存
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 2) { // 200元定金模式
if (pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
if (stock > 0) {
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
}
order(1, true, 500) // 输出:500元定金预购,得到100元优惠券
```
可以看到,这样的代码虽然能完成工作,但是难以阅读和维护。我们用职责链模式来重构这段代码
```
// 设计3种表示购买模式的节点函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串
// 'nextSuccessor'来表示该请求需要往后面继续传递
let order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
}
}
let order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
return 'nextSuccessor' // 我不知道下一个节点是谁,反正把请求往后面传递
}
}
let orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
// 把函数包装进职责链节点,定义一个构造函数Chain,在newChain的时候传递的参数即为
// 需要被包装的函数,同时它还拥有一个实例属性this.successor,表示在链中的下一个节点
let Chain = function (fn) {
this.fn = fn // 包装的函数
this.successor = null // 下一个节点
}
// 指定在链中的下一个节点
Chain.prototype.setNextSuccessor = function (successor) {
return this.successor = successor
}
// 传递请求给某个节点
Chain.prototype.passRequest = function () {
let ret = this.fn.apply(this, arguments)
if (ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
return ret
}
// 现在我们把3个订单函数分别包装成职责链的节点
let chainOrder500 = new Chain(order500)
let chainOrder200 = new Chain(order200)
let chainOrderNormal = new Chain(orderNormal)
// 然后指定节点在职责链中的顺序
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
// 最后把请求传递给第一个节点
chainOrder500.passRequest(1, true, 500) // 500元定金预购,得到100元优惠券
chainOrder500.passRequest(2, true, 500) // 200元定金预购,得到50元优惠券
chainOrder500.passRequest(3, true, 500) // 普通购买,无优惠券
chainOrder500.passRequest(1, false, 0) // 手机库存不足
// 通过改进,我们可以自由灵活地增加、移除、修改链中的节点顺序,比如某天又推出300元定金购买
let order300 = function () {
// 具体实现略
}
chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)
```
## 职责链模式的优缺点分析
职责链模式最大的优点就是解耦了请求发送者和 N 个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发送的请求,所以你只需要把请求传递给第一个节点即可。
其次,使用了职责链模式后,链中的节点可以灵活地拆分重组;另外还可以手动指定起始节点。
如果链中的节点很多,在某一次请求传递的过程中大部分节点没有起到实质性的作用,所以也要避免过长的职责链带来的性能损耗;
另外还要处理一种情况,即某个请求链中节点都无法处理,这种情况下,可以在链尾巴添加一个保底的接收者节点来处理这种请求。
# 中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者来通信,而不是相互引用。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
![](https://img.kancloud.cn/7d/33/7d332751f49cc79a91053dcbf6532467_721x401.png)
- 要注意对象之间并非一定需要解耦,在实际项目中,模块或对象之间有一些依赖关系是很正常的
- 对象之间交互的复杂性会转移成中介者对象的复杂性,使得中介者对象经常是巨大且难以维护的,且其会占去一部分的内存。
场景:一场测试结束后, 公布结果: 告知解答出题目的人挑战成功, 否则挑战失败。
```js
const player = function(name) {
this.name = name
playerMiddle.add(name)
}
player.prototype.win = function() {
playerMiddle.win(this.name)
}
player.prototype.lose = function() {
playerMiddle.lose(this.name)
}
const playerMiddle = (function() { // 将就用下这个 demo, 这个函数当成中介者
const players = []
const winArr = []
const loseArr = []
return {
add: function(name) {
players.push(name)
},
win: function(name) {
winArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
lose: function(name) {
loseArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
show: function() {
for (let winner of winArr) {
console.log(winner + '挑战成功;')
}
for (let loser of loseArr) {
console.log(loser + '挑战失败;')
}
},
}
}())
const a = new player('A 选手')
const b = new player('B 选手')
const c = new player('C 选手')
a.win()
b.win()
c.lose()
// A 选手挑战成功;
// B 选手挑战成功;
// C 选手挑战失败;
```
在这段代码中 A、B、C 之间没有直接发生关系, 而是通过另外的 playerMiddle 对象建立链接, 姑且将之当成是中介者模式了。
# 装饰者模式
给对象动态增加职责的方式称为装饰者(decorator)模式,装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
# 适配器模式
当我们试图调用模块或者对象的某个接口时,发现这个接口的格式并不符合目前的需求。这时候有两种解决办法:一是修改原来的接口实现,但如果原来的模块很复杂或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了;第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要与适配器打交道。
> 适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就是用它,因为没有人可以完全预料到未来的事情
```
const googleMap = {
show: function () {
console.log('开始渲染谷歌地图')
}
}
const baiduMap = {
show: function () {
console.log('开始渲染百度地图')
}
}
const renderMap = function (map) {
if (map.show instanceof Function) {
map.show()
}
}
renderMap(googleMap)
renderMap(baiduMap)
```
这段程序得以顺利运行的关键是 googleMap 和 baiduMap 提供了一致的 show 方法,但第三方的接口方法并不在我们自己的控制范围内,加入 baiduMap 提供的显示地图的方法不叫 show 而叫 display 呢?此时我们可以通过增加 baiduMapAdapter
来解决问题
```
const googleMap = {
show: function () {
console.log('开始渲染谷歌地图')
}
}
const baiduMap = {
display: function () {
console.log('开始渲染百度地图')
}
}
const baiduMapAdatper = {
show: function () {
return baiduMap.display()
}
}
const renderMap = function (map) {
if (map.show instanceof Function) {
map.show()
}
}
renderMap(googleMap)
renderMap(baiduMap)
```
# 其他参考资料
[https://juejin.im/post/5afe6430518825428630bc4d](https://juejin.im/post/5afe6430518825428630bc4d)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直线、矩形、多边形
- Part2-曲线图形
- Part3-线条操作
- Part4-文本操作
- Part5-图像操作
- Part6-变形操作
- Part7-像素操作
- Part8-渐变与阴影
- Part9-路径与状态
- Part10-物理动画
- Part11-边界检测
- Part12-碰撞检测
- Part13-用户交互
- Part14-高级动画
- CSS
- SCSS
- codePen
- 速查表
- 面试题
- 《CSS Secrets》
- SVG
- 移动端适配
- 滤镜(filter)的使用
- JS
- 基础概念
- 作用域、作用域链、闭包
- this
- 原型与继承
- 数组、字符串、Map、Set方法整理
- 垃圾回收机制
- DOM
- BOM
- 事件循环
- 严格模式
- 正则表达式
- ES6部分
- 设计模式
- AJAX
- 模块化
- 读冴羽博客笔记
- 第一部分总结-深入JS系列
- 第二部分总结-专题系列
- 第三部分总结-ES6系列
- 网络请求中的数据类型
- 事件
- 表单
- 函数式编程
- Tips
- JS-Coding
- Framework
- Vue
- 书写规范
- 基础
- vue-router & vuex
- 深入浅出 Vue
- 响应式原理及其他
- new Vue 发生了什么
- 组件化
- 编译流程
- Vue Router
- Vuex
- 前端路由的简单实现
- React
- 基础
- 书写规范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 与 Hook
- 《深入浅出React和Redux》笔记
- 前半部分
- 后半部分
- react-transition-group
- Vue 与 React 的对比
- 工程化与架构
- Hybird
- React Native
- 新手上路
- 内置组件
- 常用插件
- 问题记录
- Echarts
- 基础
- Electron
- 序言
- 配置 Electron 开发环境 & 基础概念
- React + TypeScript 仿 Antd
- TypeScript 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令