>[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>
```

## 緩存代理
緩存代理可以為一些開銷大的運算結果提供暫時的存儲,在下次運算時,如果傳遞進來的參數跟之前的一致,則可以直接返回前面存儲的運算結果。
通過傳入高階函數這種更加靈活的方式,可以為各種計算方法創建緩存代理。計算方法被當作參數傳入一個專門用于創建緩存代理的工廠中,這樣一來,我們就可以為加法、乘法、減法等創建緩存代理,代碼如下
```
// 計算乘積
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 中,通常我們使用一種叫做發布/訂閱模式的變體來實現觀察者模式。這兩種模式很相似,但是也有一些值得注意的不同。
- 觀察者模式要求想要接受相關通知的觀察者必須到發起這個事件的被觀察者上注冊這個事件。
- 發布/訂閱模式使用一個主題/事件頻道,這個頻道處于想要獲取通知的訂閱者和發起事件的發布者之間。這個事件系統允許代碼定義應用相關的事件,這個事件可以傳遞特殊的參數,參數中包含有訂閱者所需要的值。這種想法是為了避免訂閱者和發布者之間的依賴性。
- 這種和觀察者模式之間的不同,使訂閱者可以實現一個合適的事件處理函數,用于注冊和接受由發布者廣播的相關通知。

[模擬實現 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 個接收者之間的復雜關系,由于不知道鏈中的哪個節點可以處理你發送的請求,所以你只需要把請求傳遞給第一個節點即可。
其次,使用了職責鏈模式后,鏈中的節點可以靈活地拆分重組;另外還可以手動指定起始節點。
如果鏈中的節點很多,在某一次請求傳遞的過程中大部分節點沒有起到實質性的作用,所以也要避免過長的職責鏈帶來的性能損耗;
另外還要處理一種情況,即某個請求鏈中節點都無法處理,這種情況下,可以在鏈尾巴添加一個保底的接收者節點來處理這種請求。
# 中介者模式
中介者模式的作用就是解除對象與對象之間的緊耦合關系。增加一個中介者對象后,所有的相關對象都通過中介者來通信,而不是相互引用。中介者模式使網狀的多對多關系變成了相對簡單的一對多關系。

- 要注意對象之間并非一定需要解耦,在實際項目中,模塊或對象之間有一些依賴關系是很正常的
- 對象之間交互的復雜性會轉移成中介者對象的復雜性,使得中介者對象經常是巨大且難以維護的,且其會占去一部分的內存。
場景:一場測試結束后, 公布結果: 告知解答出題目的人挑戰成功, 否則挑戰失敗。
```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 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs