21種JavaScript設計模式最新記錄(含圖和示例)
最近觀看了《Javascript設計模式系統講解與應用》教程,對設計模式有了新的認識,特在此做些記錄。
一、UML
文中會涉及眾多的UML類圖,在開篇需要做點基礎概念的認識。以下面的圖為例,圖片和說明均來源於《大話設計模式》一書。
(1)矩形框,它代表一個類。類圖分三層,第一層顯示類的名稱,如果是抽象類,則用斜體顯示。第二層是類的特性,通常就是欄位和屬性。第三層是類的操作,通常是方法或行為。前面的符號,+ 表示public,- 表示private,# 表示protected。
(2)矩形框的頂端包含<<interface>>就表示一個介面。第一行是介面名稱,第二行是介面方法。
(3)繼承關係用空心三角形 + 實線來表示的。
(4)實現介面用空心三角形 + 虛線來表示。
(5)當一個類知道另一個類時,可以用關聯(Association),關聯關係用實線箭頭來表示。
(6)聚合(Aggregation)表示一種弱的擁有關係,體現的是A物件可以包含B物件,但B物件不是A物件的一部分。聚合關係用空心的菱形 + 實線箭頭來表示。
(7)組合(Composition)是一種強的擁有關係,體現了嚴格的部分和整體的關係,組合關係用實心的菱形 + 實線箭頭來表示。
(8)依賴(Dependency)關係,用虛線箭頭來表示。
二、五大原則(SOLID)
作者認為設計模式得分成兩部分,第一是設計(即設計原則),第二才是模式。五大原則是設計模式的基礎,SOLID是五大原則的首字母簡寫,作者在介紹每個模式的時候,都會提到是否符合這五大原則。
(1)S-單一職責原則,一個程式只做一件事,如果功能過於複雜,那麼就拆分,保持獨立。
(2)O-開放封閉原則,對擴充套件開發,對修改封閉,增加需求就擴充套件新程式碼,而非修改已有程式碼。修改已有程式碼不但需要測試,成本高昂,而且多人開發很容易發生衝突。
(3)L-里氏替換原則,子類能覆蓋父類,父類能出現的地方子類就能出現。
(4)I-介面分離原則,保持介面的單一獨立,避免胖介面。
(5)D-依賴倒置原則,面向介面程式設計,依賴於抽象而不依賴於具體,使用方只關注介面而不關注具體類的實現。
在JavaScript中,SO體現較多;而LID體現較少,因為JavaScript的語法限制,例如弱型別、沒有介面等。如果用Promise來說明SO,那麼就是:
(1)S:每個then中的邏輯只做好一件事。
(2)O:如果新增需求,那麼就擴充套件then。
說句題外話,TypeScript是一門強型別面嚮物件語言,在它問世後,就可以完完整整的應用這五大原則,不會有什麼限制。前段時間寫過幾篇TypeScript的簡單教程,有興趣的可以參考。
三、21種設計模式
1)工廠模式
將new操作單獨封裝。遇到new時,就要考慮是否該使用工廠模式。例如購買漢堡直接點餐,不用自己製作,而商店要封裝做漢堡的操作,然後賣給客戶。下面是一個簡單示例。
class Creator { create(name) { return new Product(name); } } let creator = new Creator(); let p = creator.create("p");
使用場景包括jQuery的$("div")、React.createElement()和Vue的非同步元件。
2)單例模式
系統中被唯一使用,一個類只有一個例項,例如登入框、購物車。單例模式需要用到private修飾符,但ES6中沒有,可用閉包模擬,如下所示。
class SingleObject { login() { console.log("login..."); } } SingleObject.getInstance = (function() { let instance; return function() { if (!instance) { instance = new SingleObject(); } return instance; }; })(); let obj1 = SingleObject.getInstance(); obj1.login();
使用場景包括jQuery中的$(只有一個)、模擬登入框和Redux中的Store。
3)介面卡模式
舊介面格式和使用者不相容,中間加一個適配轉換介面。以轉換插頭為例,如下所示。
class Adaptee { specificRequest() { return "德國標準"; } } class Target { constructor() { this.adaptee = new Adaptee(); } request() { let info = this.adaptee.specificRequest(); return `${info}--轉換--中國標準`; } } let target = new Target(); target.request();
使用場景包括封裝舊介面、Vue的計算屬性。
4)裝飾器模式
為物件新增新功能,不改變其原有的結構和功能。與介面卡模式不同,之前的介面仍然可以使用。裝飾器模式類似於生活中的手機殼,不僅保護了手機,還不影響音量鍵、揚聲器等部分。
使用場景包括ES7裝飾器、core-decorator,下面通過裝飾器語法演示呼叫方法時自動打日誌。
function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function() { console.log(`Calling ${name} with`, arguments); return oldValue.apply(this, arguments); }; return descriptor; } class Math { @log add(a, b) { return a + b; } } const math = new Math(); math.add(2, 4);
裝飾器的原理如下所示。
@decorator class A {} //等同於 class A {} A = decorator(A) || A;
5)代理模式
當使用者無權訪問目標物件時,可在中間加代理,通過代理做授權和控制,類似於明星的經紀人。
使用場景包括DOM的事件委託、jQuery的$.proxy、ES6的proxy。下面通過代理語法模擬明星和經紀人。
let star = { name: "張XX", // 明星 age: 25, phone: "13800138000" }; let agent = new Proxy(star, { // 經紀人 get: function(target, key) { if (key === "phone") { // 返回經紀人自己的手機號 return "18012345678"; } if (key === "price") { // 明星不報價,經紀人報價 return 120000; } return target[key]; }, set: function(target, key, val) { if (key === "customPrice") { if (val < 100000) { throw new Error("價格太低"); } else { target[key] = val; return true; } } } });
代理模式提供一模一樣的介面,而介面卡模式提供不同的介面。代理模式直接針對原有功能,不過需要經過限制或閹割;裝飾器模式能擴充套件功能,而原有功能不變且可直接使用。
6)外觀模式
為子系統中的一組介面提供一個高層介面,使用者使用這個高層介面。下面是外觀模式的示意圖,去醫院看病通常是左邊的情況,但如果有接待員,那麼就會是右邊的情況,由他去掛號、門診、取藥等。
外觀模式的UML類圖是下面這樣。
使用場景包括同一個函式可接收不同組合的引數,如下所示,在jQuery中提供了很多這類方法。
function bindEvent(elem, type, selector, fn) { if (fn == null) { fn = selector; selector = null; } //... } //呼叫可以有兩種 bindEvent(elem, "click", "#div", fn); bindEvent(elem, "click", fn);
注意,外觀模式雖然好用,但是不符合單一職責和開放封閉原則,需要謹慎使用。
7)觀察者模式
觀察者模式是一種釋出訂閱機制,當物件間存在一對多(也可以一對一)的關係時,可使用觀察者模式。在使用觀察者模式後,當一個物件被修改時,就會自動通知它的依賴物件。
生活中的點咖啡也是一種觀察者模式,當點好後,就找位子坐下,等叫號或者等服務員送過來。下面是個簡單的示例。
class Subject { // 主題,接收狀態變化,觸發每個觀察者 constructor() { this.state = 0; this.observers = []; } getState() { //獲取狀態 return this.state; } setState(state) { //修改狀態 this.state = state; this.notifyAllObservers(); } attach(observer) { //新增觀察者 this.observers.push(observer); } notifyAllObservers() { //通知所有觀察者 this.observers.forEach(observer => { observer.update(); }); } } class Observer { // 觀察者,等待被觸發 constructor(name, subject) { this.name = name; this.subject = subject; this.subject.attach(this); } update() { console.log(`${this.name} update, state: ${this.subject.getState()}`); } } let s = new Subject(); let o1 = new Observer("o1", s); let o2 = new Observer("o2", s); s.setState(1); s.setState(2);
使用場景包括DOM事件繫結、Promise、jQuery callbacks和Node.js自定義事件。
8)迭代器模式
訪問一個有序集合(不包括物件),使用者無需知道集合的內部結構。下面是一個模擬的迭代器(Iterator),包含next()和hasNext()方法。
class Iterator { constructor(conatiner) { this.list = conatiner.list; this.index = 0; } next() { if (this.hasNext()) { return this.list[this.index++]; } return null; } hasNext() { if (this.index >= this.list.length) { return false; } return true; } } class Container { constructor(list) { this.list = list; } getIterator() { return new Iterator(this); } } let container = new Container([1, 2, 3, 4, 5]); let iterator = container.getIterator(); while (iterator.hasNext()) { console.log(iterator.next()); }
使用場景包括jQuery的each、ES6的Iterator。ES6之所以引入Iterator有以下三個原因:
(1)實現了迭代器模式。
(2)ES6語法中,有序集合的資料型別已經有很多,例如Array、Map、Set、String、TypedArray、arguments和NodeList。
(3)需要有一個統一的介面來遍歷所有資料型別。
注意,Object不是有序集合,可用Map替代。
9)狀態模式
一個物件會有狀態變化,每次狀態變化都會觸發一個邏輯,而這個邏輯不能總是用if...else語句來控制。生活中的交通訊號燈是一種狀態模式,當顏色變化時,會有不同的結果,例如紅燈禁止、綠燈通行。
使用場景包括Promise原理、有限狀態機。下面的示例使用了開源的javascript-state-machine。
var fsm = new StateMachine({ init: "solid", transitions: [ { name: "melt", from: "solid", to: "liquid" }, { name: "freeze", from: "liquid", to: "solid" }, { name: "vaporize", from: "liquid", to: "gas" }, { name: "condense", from: "gas", to: "liquid" } ], methods: { onMelt: function() { console.log("I melted"); }, onFreeze: function() { console.log("I froze"); }, onVaporize: function() { console.log("I vaporized"); }, onCondense: function() { console.log("I condensed"); } } });
10)原型模式
克隆自己,生成一個新物件。Java預設有clone介面,不用自定義。而JavaScript中的Object.create()採用了原型模式的思想,如下所示。
const person = { isHuman: false, printIntroduction: function () { console.log(`My name is ${this.name}`); } }; const me = Object.create(person); me.name = "Matthew"; me.isHuman = true;
11)橋接模式
將抽象化與實現化兩者解耦,使得它們可以獨立變化。
下圖和分析均來源於《C#設計模式(10)——橋接模式》,在用橋接模式優化後,就能將形狀和顏色通過繼承生產的強耦合關係改成弱耦合的關聯關係,這裡採用了組合大於繼承的思想。
如果想新增一個五角星,只需要新增一個形狀類的子類五角星接即可,不需要再去新增各種顏色的具體五角星了。如果想要一個藍色五角星就將五角星和藍色進行組合來獲取。這樣設計降低了形狀和顏色的耦合,減少了具體子類的種類。
12)組合模式
生成樹形結構,表示“整體-部分”關係。讓整體和部分都具有一致的操作方式。虛擬DOM中的vnode是這種形式(如下所示),但資料型別比較簡單。
{ tag: "div", attr: { className: "container" }, children: [ { tag: "p", attr: {}, children: ["123"] }, { tag: "p", attr: {}, children: ["456"] } ] }
13)享元模式
享元是個組合詞,分為共享和元資料。享元模式關注共享記憶體,考慮記憶體而非效率。
在JavaScript中不用刻意的考慮記憶體問題,因此沒有享元模式的直接場景,但可以找到意義相關(即共享資料開銷思想)的場景,例如事件委託。
14)策略模式
不同策略分開處理,避免出現大量if...else或者switch語句,下面是個示例。
class User { constructor(type) { this.type = type; } buy() { if (this.type == "ordinary") { console.log("普通使用者購買"); } else if (this.type == "member") { console.log("會員使用者購買"); } else if (this.type == "vip") { console.log("VIP使用者購買"); } } } //策略模式生成三個類,各自實現buy()方法 class UserOrdinary { buy() { console.log("普通使用者購買"); } } class UserMember { buy() { console.log("會員使用者購買"); } } class UserVip { buy() { console.log("VIP使用者購買"); } }
15)模板方法模式
如果程式碼中有幾步處理,可以用一個方法將它們封裝在一個方法中(如下所示),在方法中可對順序或特殊邏輯進行處理,對外統一輸出,有效降低了耦合度。
class Action { handle() { handle1(); handle2(); handle3(); } handle1() { } handle2() { } handle3() { } }
16)職責鏈模式
一步操作可能分為多個職責角色來完成,然後把這些角色都分開,再用一個鏈串起來,並且將發起者和各個處理者進行隔離。例如請假審批,需要逐級審批,如下所示。
class Action { constructor(name) { this.name = name; this.nextAction = null; } setNextAction(action) { this.nextAction = action; } handle() { console.log(`${this.name} 審批`); if (this.nextAction != null) { this.nextAction.handle(); } } } let a1 = new Action("組長"); let a2 = new Action("經理"); let a3 = new Action("總監"); a1.setNextAction(a2); a2.setNextAction(a3); a1.handle();
職責鏈模式和業務結合較多,能聯想到鏈式操作,例如jQuery的鏈式操作、Promise.then的鏈式操作。
17)命令模式
執行命令時,釋出者和執行者分開。中間加入命令物件,作為中轉站。釋出者相當於將軍,他會讓旗手或號手將命令傳達給普通士兵,如下所示。
class Receiver { exec() { console.log("執行"); } } class Command { constructor(receiver) { this.receiver = receiver; } cmd() { console.log("觸發命令"); this.receiver.exec(); } } class Invoker { constructor(command) { this.command = command; } invoke() { console.log("開始"); this.command.cmd(); } } let solider = new Receiver(); //士兵 let trumpeter = new Command(solider); //小號手 let general = new Invoker(trumpeter); //將軍 general.invoke();
使用場景包括網頁富文字編輯器,由瀏覽器封裝一個命令物件,如下所示。
document.execCommand("bold"); document.execCommand("undo");
18)備忘錄模式
隨時記錄一個物件的狀態變化,隨時可以恢復之前的某個狀態,例如網頁編輯器中的撤銷功能。
19)中介者模式
當有許多物件,並且物件之間會頻繁的相互訪問(如左圖)時,就會變得很混亂,而物件之間的訪問都由中介者代轉(如右圖),就會變得很有條理,不會出現牽一髮動全身的情況。
生活中的房屋中介就採用了這種模式,如下所示。
class Mediator { //中介者 constructor(a, b) { this.a = a; this.b = b; } setA() { let number = this.b.number; this.a.setNumber(number * 100); } setB() { let number = this.a.number; this.b.setNumber(number / 100); } } class A { constructor() { this.number = 0; } setNumber(num, m) { this.number = num; if (m) { m.setB(); //通過中介者修改 } } } class B { constructor() { this.number = 0; } setNumber(num, m) { this.number = num; if (m) { m.setA(); //通過中介者修改 } } }
20)訪問者模式
將資料操作和資料結構進行分離。
21)直譯器模式
描述語言語法如何定義,如何解釋和編譯,例如Babe