1. 設計模式簡介
[設計模式](Design pattern) 是解決軟件開發某些特定問題而提出的一些解決方案也可以理解成解決問題的一些思路。通過設計模式可以幫助我們增強代碼的[可重用性]、可擴充性、 可維護性、靈活性好。我們使用設計模式最終的目的是實現代碼的 [高內聚] 和 低耦合。通俗一點講的話 打比方面試官經常會問你如何讓代碼有健壯性。其實把代碼中的變與不變分離,確保變化的部分靈活、不變的部分穩定,這樣的封裝變化就是代碼健壯性的關鍵。而設計模式的出現,就是幫我們寫出這樣的代碼。 設計模式就是解決編程里某類問題的通用模板,總結出來的代碼套路就是設計模式。本文章總結下JS在工作中常用的設計模式 ,以幫助大家提高代碼性能,增強工作效率!
設計模式原則
-
S – Single Responsibility Principle 單一職責原則
-
一個程序只做好一件事
-
如果功能過于復雜就拆分開,每個部分保持獨立
-
-
O – OpenClosed Principle 開放/封閉原則
-
對擴展開放,對修改封閉
-
增加需求時,擴展新代碼,而非修改已有代碼
-
-
L – Liskov Substitution Principle 里氏替換原則
-
子類能覆蓋父類
-
父類能出現的地方子類就能出現
-
-
I – Interface Segregation Principle 接口隔離原則
-
保持接口的單一獨立
-
類似單一職責原則,這里更關注接口
-
-
D – Dependency Inversion Principle 依賴倒轉原則
-
面向接口編程,依賴于抽象而不依賴于具體
-
使用方只關注接口而不關注具體類的實現
-
SO體現較多,舉個例子:(比如Promise)
-
單一職責原則:每個then中的邏輯只做好一件事
-
開放封閉原則(對擴展開放,對修改封閉):如果新增需求,擴展then
再舉個例子:
//checkType('165226226326','mobile')
//result:false
let checkType=function(str, type) {switch (type) {case 'email':return /^[\w-]+(.[\w-]+)*@[\w-]+(.[\w-]+)+$/.test(str)case 'mobile':return /^1[3|4|5|7|8][0-9]{9}$/.test(str);case 'tel':return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);default:return true;}
}
有以下兩個問題:
-
如果想添加其他規則就得在函數里面增加 case 。添加一個規則就修改一次!這樣違反了開放-封閉原則(對擴展開放,對修改關閉)。而且這樣也會導致整個 API 變得臃腫,難維護。
-
比如A頁面需要添加一個金額的校驗,B頁面需要一個日期的校驗,但是金額的校驗只在A頁面需要,日期的校驗只在B頁面需要。如果一直添加 case 。就是導致A頁面把只在B頁面需要的校驗規則也添加進去,造成不必要的開銷。B頁面也同理。
建議的方式是給這個 API 增加一個擴展的接口:
let checkType=(function(){let rules={email(str){return /^[\w-]+(.[\w-]+)*@[\w-]+(.[\w-]+)+$/.test(str);},mobile(str){return /^1[3|4|5|7|8][0-9]{9}$/.test(str);}};//暴露接口return {//校驗check(str, type){return rules[type]?rules[type](str):false;},//添加規則addRule(type,fn){rules[type]=fn;}}
})();//調用方式
//使用mobile校驗規則
console.log(checkType.check('188170239','mobile'));
//添加金額校驗規則
checkType.addRule('money',function (str) {return /^[0-9]+(.[0-9]{2})?$/.test(str)
});
//使用金額校驗規則
console.log(checkType.check('18.36','money'));
2. 設計模式分類(23種設計模式)
-
創建型
-
單例模式(Singleton Pattern)
-
原型模式(Prototype Pattern)
-
工廠模式(Factory Method Pattern)
-
抽象工廠模式(Abstract Factory Pattern)
-
建造者模式(Builder Pattern)
-
-
結構型
-
適配器模式(Adapter Pattern)
-
裝飾器模式(Decorator Pattern)
-
代理模式(Proxy Pattern)
-
外觀模式(Facade Pattern)
-
橋接模式(Bridge Pattern)
-
組合模式(Composite Pattern)
-
享元模式(Flyweight Pattern)
-
-
行為型
-
觀察者模式(Observer Pattern)
-
迭代器模式(Iterator Pattern)
-
策略模式(Strategy Pattern)
-
模板方法模式(Template Method Pattern)
-
責任鏈模式(Chain of Responsibility)
-
命令模式(Command)
-
備忘錄模式(Memento)
-
狀態模式(State)
-
訪問者模式(Visitor)
-
中介者模式(Mediator)
-
解釋器模式(Interpreter)
-
2.1 工廠方法模式(Factory Method Pattern)
工廠模式定義一個用于創建對象的接口,這個接口由子類決定實例化哪一個類。該模式使一個類的實例化延遲到了子類。而子類可以重寫接口方法以便創建的時候指定自己的對象類型。
class Product {constructor(name) {this.name = name}init() {console.log('init')}fun() {console.log('fun')}
}class Factory {create(name) {return new Product(name)}
}// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
適用場景
-
如果你不想讓某個子系統與較大的那個對象之間形成強耦合,而是想運行時從許多子系統中進行挑選的話,那么工廠模式是一個理想的選擇
-
將new操作簡單封裝,遇到new的時候就應該考慮是否用工廠模式;
-
需要依賴具體環境創建不同實例,這些實例都有相同的行為,這時候我們可以使用工廠模式,簡化實現的過程,同時也可以減少每種對象所需的代碼量,有利于消除對象間的耦合,提供更大的靈活性
優點
-
創建對象的過程可能很復雜,但我們只需要關心創建結果。
-
構造函數和創建者分離, 符合“開閉原則”
-
一個調用者想創建一個對象,只要知道其名稱就可以了。
-
擴展性高,如果想增加一個產品,只要擴展一個工廠類就可以。
缺點
-
添加新產品時,需要編寫新的具體產品類,一定程度上增加了系統的復雜度
-
考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度
什么時候不用
當被應用到錯誤的問題類型上時,這一模式會給應用程序引入大量不必要的復雜性.除非為創建對象提供一個接口是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
由于對象的創建過程被高效的抽象在一個接口后面的事實,這也會給依賴于這個過程可能會有多復雜的單元測試帶來問題。
例子
-
曾經我們熟悉的JQuery的$()就是一個工廠函數,它根據傳入參數的不同創建元素或者去尋找上下文中的元素,創建成相應的jQuery對象
class jQuery {constructor(selector) {super(selector)}add() {}// 此處省略若干API
}window.$ = function(selector) {return new jQuery(selector)
}
-
vue 的異步組件
在大型應用中,我們可能需要將應用分割成小一些的代碼塊,并且只在需要的時候才從服務器加載一個模塊。為了簡化,Vue 允許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。Vue 只有在這個組件需要被渲染的時候才會觸發該工廠函數,且會把結果緩存起來供未來重渲染。例如:
Vue.component('async-example', function (resolve, reject) {setTimeout(function () {// 向 `resolve` 回調傳遞組件定義resolve({template: '<div>I am async!</div>'})}, 1000)
})
2.2 單例模式(Singleton Pattern)
一個類只有一個實例,并提供一個訪問它的全局訪問點。
class LoginForm {constructor() {this.state = 'hide'}show() {if (this.state === 'show') {alert('已經顯示')return}this.state = 'show'console.log('登錄框顯示成功')}hide() {if (this.state === 'hide') {alert('已經隱藏')return}this.state = 'hide'console.log('登錄框隱藏成功')}}LoginForm.getInstance = (function () {let instancereturn function () {if (!instance) {instance = new LoginForm()}return instance}})()let obj1 = LoginForm.getInstance()
obj1.show()let obj2 = LoginForm.getInstance()
obj2.hide()console.log(obj1 === obj2)
優點
-
劃分命名空間,減少全局變量
-
增強模塊性,把自己的代碼組織在一個全局變量名下,放在單一位置,便于維護
-
且只會實例化一次。簡化了代碼的調試和維護
缺點
-
由于單例模式提供的是一種單點訪問,所以它有可能導致模塊間的強耦合 從而不利于單元測試。無法單獨測試一個調用了來自單例的方法的類,而只能把它與那個單例作為一個單元一起測試。
場景例子
-
定義命名空間和實現分支型方法
-
登錄框
-
vuex 和 redux中的store
2.3 建造者模式(Builder Pattern)
建造者模式是一種對象創建設計模式,它旨在通過一步步的構建流程來創建復雜對象
。其代碼示例如下:
// 創建 Product 類
class Sandwich {constructor() {this.ingredients = [];}addIngredient(ingredient) {this.ingredients.push(ingredient);}toString() {return this.ingredients.join(', ');}
}// 創建一個建造者類
class SandwichBuilder {constructor() {this.sandwich = new Sandwich();}reset() {this.sandwich = new Sandwich();}putMeat(meat) {this.sandwich.addIngredient(meat);}putCheese(cheese) {this.sandwich.addIngredient(cheese);}putVegetables(vegetables) {this.sandwich.addIngredient(vegetables);}get result() {return this.sandwich;}
}// 創建用戶(director)使用的 builder
class SandwichMaker {constructor() {this.builder = new SandwichBuilder();}createCheeseSteakSandwich() {this.builder.reset();this.builder.putMeat('ribeye steak');this.builder.putCheese('american cheese');this.builder.putVegetables(['peppers', 'onions']);return this.builder.result;}
}// 建造一個三明治
const sandwichMaker = new SandwichMaker();
const sandwich = sandwichMaker.createCheeseSteakSandwich();
console.log(`Your sandwich: ${sandwich}`); // Output: Your sandwich: ribeye steak, american cheese, peppers, onions
2.4 抽象工廠模式(Abstract Factory Pattern)
抽象工廠模式提供了一種封裝一組具有相同主題的單個工廠的方式
。它有一個接口,用于創建相關或依賴對象的家族,而不需要指定實際實現的類
。其代碼示例如下:
// 創建一組主題對象類型的抽象類
class AnimalFood {provide() {throw new Error('This method must be implemented.');}
}class AnimalToy {provide() {throw new Error('This method must be implemented.');}
}// 創建一組具體代表家族的對象
class HighQualityDogFood extends AnimalFood {provide() {return 'High quality dog food';}
}class HighQualityDogToy extends AnimalToy {provide() {return 'High quality dog toy';}
}class CheapCatFood extends AnimalFood {provide() {return 'Cheap cat food';}
}class CheapCatToy extends AnimalToy {provide() {return 'Cheap cat toy';}
}// 創建一個抽象工廠
class AnimalProductsAbstractFactory {createFood() {throw new Error('This method must be implemented.');}createToy() {throw new Error('This method must be implemented.');}
}// 創建具體工廠類
class HighQualityAnimalProductsFactory extends AnimalProductsAbstractFactory {createFood() {return new HighQualityDogFood();}createToy() {return new HighQualityDogToy();}
}class CheapAnimalProductsFactory extends AnimalProductsAbstractFactory {createFood() {return new CheapCatFood();}createToy() {return new CheapCatToy();}
}// 使用具體工廠類來創建相關的對象
const highQualityAnimalProductsFactory = new HighQualityAnimalProductsFactory();
console.log(highQualityAnimalProductsFactory.createFood().provide()); // Output: High quality dog food
console.log(highQualityAnimalProductsFactory.createToy().provide()); // Output: High quality dog toyconst cheapAnimalProductsFactory = new CheapAnimalProductsFactory();
console.log(cheapAnimalProductsFactory.createFood().provide()); // Output: Cheap cat food
console.log(cheapAnimalProductsFactory.createToy().provide()); // Output: Cheap cat toy
2.5 適配器模式(Adapter Pattern)
將一個類的接口轉化為另外一個接口,以滿足用戶需求,使類之間接口不兼容問題通過適配器得以解決。
class Plug {getName() {return 'iphone充電頭';}
}class Target {constructor() {this.plug = new Plug();}getName() {return this.plug.getName() + ' 適配器Type-c充電頭';}
}let target = new Target();
target.getName(); // iphone充電頭 適配器轉Type-c充電頭
優點
-
可以讓任何兩個沒有關聯的類一起運行。
-
提高了類的復用。
-
適配對象,適配庫,適配數據
缺點
-
額外對象的創建,非直接調用,存在一定的開銷(且不像代理模式在某些功能點上可實現性能優化)
-
如果沒必要使用適配器模式的話,可以考慮重構,如果使用的話,盡量把文檔完善
場景
-
整合第三方SDK
-
封裝舊接口
// 自己封裝的ajax, 使用方式如下
ajax({url: '/getData',type: 'Post',dataType: 'json',data: {test: 111}
}).done(function() {})
// 因為歷史原因,代碼中全都是:
// $.ajax({....})// 做一層適配器
var $ = {ajax: function (options) {return ajax(options)}
}
-
vue的computed
<template><div id="example"><p>Original message: "{{ message }}"</p> <!-- Hello --><p>Computed reversed message: "{{ reversedMessage }}"</p> <!-- olleH --></div>
</template>
<script type='text/javascript'>export default {name: 'demo',data() {return {message: 'Hello'}},computed: {reversedMessage: function() {return this.message.split('').reverse().join('')}}}
</script>
原有data 中的數據不滿足當前的要求,通過計算屬性的規則來適配成我們需要的格式,對原有數據并沒有改變,只改變了原有數據的表現形式。
不同點
適配器與代理模式相似
-
適配器模式: 提供一個不同的接口(如不同版本的插頭)
-
代理模式: 提供一模一樣的接口
2.6 裝飾器模式(Decorator Pattern)
-
動態地給某個對象添加一些額外的職責,,是一種實現繼承的替代方案
-
在不改變原對象的基礎上,通過對其進行包裝擴展,使原有對象可以滿足用戶的更復雜需求,而不會影響從這個類中派生的其他對象
class Cellphone {create() {console.log('生成一個手機')}
}
class Decorator {constructor(cellphone) {this.cellphone = cellphone}create() {this.cellphone.create()this.createShell(cellphone)}createShell() {console.log('生成手機殼')}
}
// 測試代碼
let cellphone = new Cellphone()
cellphone.create()console.log('------------')
let dec = new Decorator(cellphone)
dec.create()
場景例子
-
比如現在有4 種型號的自行車,我們為每種自行車都定義了一個單 獨的類。現在要給每種自行車都裝上前燈、尾 燈和鈴鐺這3 種配件。如果使用繼承的方式來給 每種自行車創建子類,則需要 4×3 = 12 個子類。 但是如果把前燈、尾燈、鈴鐺這些對象動態組 合到自行車上面,則只需要額外增加3 個類
-
ES7 Decorator
-
core-decorators
優點
-
裝飾類和被裝飾類都只關心自身的核心業務,實現了解耦。
-
方便動態的擴展功能,且提供了比繼承更多的靈活性。
缺點
-
多層裝飾比較復雜。
-
常常會引入許多小對象,看起來比較相似,實際功能大相徑庭,從而使得我們的應用程序架構變得復雜起來
2.7 代理模式(Proxy Pattern)
是為一個對象提供一個代用品或占位符,以便控制對它的訪問
假設當A 在心情好的時候收到花,小明表白成功的幾率有 60%,而當A 在心情差的時候收到花,小明表白的成功率無限趨近于0。 小明跟A 剛剛認識兩天,還無法辨別A 什么時候心情好。如果不合時宜地把花送給A,花 被直接扔掉的可能性很大,這束花可是小明吃了7 天泡面換來的。 但是A 的朋友B 卻很了解A,所以小明只管把花交給B,B 會監聽A 的心情變化,然后選 擇A 心情好的時候把花轉交給A,代碼如下:
let Flower = function() {}
let xiaoming = {sendFlower: function(target) {let flower = new Flower()target.receiveFlower(flower)}
}
let B = {receiveFlower: function(flower) {A.listenGoodMood(function() {A.receiveFlower(flower)})}
}
let A = {receiveFlower: function(flower) {console.log('收到花'+ flower)},listenGoodMood: function(fn) {setTimeout(function() {fn()}, 1000)}
}
xiaoming.sendFlower(B)
場景
-
HTML元 素事件代理
<ul id="ul"><li>1</li><li>2</li><li>3</li>
</ul>
<script>let ul = document.querySelector('#ul');ul.addEventListener('click', event => {console.log(event.target);});
</script>
-
ES6 的 proxy
-
jQuery.proxy()方法
優點
-
代理模式能將代理對象與被調用對象分離,降低了系統的耦合度。代理模式在客戶端和目標對象之間起到一個中介作用,這樣可以起到保護目標對象的作用
-
代理對象可以擴展目標對象的功能;通過修改代理對象就可以了,符合開閉原則;
缺點
處理請求速度可能有差別,非直接訪問存在開銷
不同點
裝飾者模式實現上和代理模式類似
-
裝飾者模式: 擴展功能,原有功能不變且可直接使用
-
代理模式: 顯示原有功能,但是經過限制之后的
2.8?外觀模式(Facade Pattern)
為子系統的一組接口提供一個一致的界面,定義了一個高層接口,這個接口使子系統更加容易使用
1. 兼容瀏覽器事件綁定
let addMyEvent = function (el, ev, fn) {if (el.addEventListener) {el.addEventListener(ev, fn, false)} else if (el.attachEvent) {el.attachEvent('on' + ev, fn)} else {el['on' + ev] = fn}
};
2. 封裝接口
let myEvent = {// ...stop: e => {e.stopPropagation();e.preventDefault();}
};
場景
-
設計初期,應該要有意識地將不同的兩個層分離,比如經典的三層結構,在數據訪問層和業務邏輯層、業務邏輯層和表示層之間建立外觀Facade
-
在開發階段,子系統往往因為不斷的重構演化而變得越來越復雜,增加外觀Facade可以提供一個簡單的接口,減少他們之間的依賴。
-
在維護一個遺留的大型系統時,可能這個系統已經很難維護了,這時候使用外觀Facade也是非常合適的,為系系統開發一個外觀Facade類,為設計粗糙和高度復雜的遺留代碼提供比較清晰的接口,讓新系統和Facade對象交互,Facade與遺留代碼交互所有的復雜工作。
參考: 大話設計模式
優點
-
減少系統相互依賴。
-
提高靈活性。
-
提高了安全性
缺點
-
不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
2.9 觀察者模式(Observer Pattern)
定義了一種一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使它們能夠自動更新自己,當一個對象的改變需要同時改變其它對象,并且它不知道具體有多少對象需要改變的時候,就應該考慮使用觀察者模式。
-
發布 & 訂閱
-
一對多
// 主題 保存狀態,狀態變化之后觸發所有觀察者對象
class Subject {constructor() {this.state = 0this.observers = []}getState() {return this.state}setState(state) {this.state = statethis.notifyAllObservers()}notifyAllObservers() {this.observers.forEach(observer => {observer.update()})}attach(observer) {this.observers.push(observer)}
}// 觀察者
class Observer {constructor(name, subject) {this.name = namethis.subject = subjectthis.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('02', s)s.setState(12)
場景
-
DOM事件
document.body.addEventListener('click', function() {console.log('hello world!');
});
document.body.click()
-
vue 響應式
優點
-
支持簡單的廣播通信,自動通知所有已經訂閱過的對象
-
目標對象與觀察者之間的抽象耦合關系能單獨擴展以及重用
-
增加了靈活性
-
觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴于抽象,而不是依賴于具體。從而使得各自的變化都不會影響到另一邊的變化。
缺點
過度使用會導致對象與對象之間的聯系弱化,會導致程序難以跟蹤維護和理解
2.10 狀態模式(State)
允許一個對象在其內部狀態改變的時候改變它的行為,對象看起來似乎修改了它的類
// 狀態 (弱光、強光、關燈)
class State {constructor(state) {this.state = state}handle(context) {console.log(`this is ${this.state} light`)context.setState(this)}
}
class Context {constructor() {this.state = null}getState() {return this.state}setState(state) {this.state = state}
}
// test
let context = new Context()
let weak = new State('weak')
let strong = new State('strong')
let off = new State('off')// 弱光
weak.handle(context)
console.log(context.getState())// 強光
strong.handle(context)
console.log(context.getState())// 關閉
off.handle(context)
console.log(context.getState())
場景
-
一個對象的行為取決于它的狀態,并且它必須在運行時刻根據狀態改變它的行為
-
一個操作中含有大量的分支語句,而且這些分支語句依賴于該對象的狀態
優點
-
定義了狀態與行為之間的關系,封裝在一個類里,更直觀清晰,增改方便
-
狀態與狀態間,行為與行為間彼此獨立互不干擾
-
用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目了然
缺點
-
會在系統中定義許多狀態類
-
邏輯分散
2.11 迭代器模式(Iterator Pattern)
提供一種方法順序一個聚合對象中各個元素,而又不暴露該對象的內部表示。
class Iterator {constructor(conatiner) {this.list = conatiner.listthis.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())
}
場景例子
-
Array.prototype.forEach
-
jQuery中的$.each()
-
ES6 Iterator
特點
-
訪問一個聚合對象的內容而無需暴露它的內部表示。
-
為遍歷不同的集合結構提供一個統一的接口,從而支持同樣的算法在不同的集合結構上進行操作
總結
對于集合內部結果常常變化各異,不想暴露其內部結構的話,但又想讓客戶代碼透明的訪問其中的元素,可以使用迭代器模式
2.12 橋接模式(Bridge Pattern)
橋接模式(Bridge)將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
class Color {constructor(name){this.name = name}
}
class Shape {constructor(name,color){this.name = namethis.color = color }draw(){console.log(`${this.color.name} ${this.name}`)}
}//測試
let red = new Color('red')
let yellow = new Color('yellow')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()
優點
-
有助于獨立地管理各組成部分, 把抽象化與實現化解耦
-
提高可擴充性
缺點
-
大量的類將導致開發成本的增加,同時在性能方面可能也會有所減少。
?團隊介紹
「三翼鳥數字化技術平臺-ToC服務平臺」以用戶行為數據為基礎,利用推薦引擎為用戶提供“千人千面”的個性化推薦服務,改善用戶體驗,持續提升核心業務指標。通過構建高效、智能的線上運營系統,全面整合數據資產,實現數據分析-人群圈選-用戶觸達-后效分析-策略優化的運營閉環,并提供可視化報表,一站式操作提升數字化運營效率。
?