開門見山
我們都知道 javascript
是一種基于原型的弱類型語言,擁有動態(tài)數(shù)據(jù)類型,靈活多變。因此,相比于傳統(tǒng)的 java
,c++
來說, javascript
面向?qū)ο蟮脑O(shè)計模式會有點牽強(qiáng)。
但這也并不妨礙我們學(xué)習(xí)使用 javascript
來了解設(shè)計模式思想及其設(shè)計理念。因為在我看來,這屬于程序員的 “內(nèi)功” ,只有 “內(nèi)功” 修煉得當(dāng),才能更好的學(xué)習(xí) “上乘武功”。
更何況,現(xiàn)在很多框架的源碼都引入了非常多的設(shè)計模式思想,例如 發(fā)布訂閱模式,單例模式,工程模式等比比皆是。
因此,想要學(xué)習(xí)框架源碼,編寫高質(zhì)量,易維護(hù)的代碼,設(shè)計模式的學(xué)習(xí)是必不可少的。今天我們就聊一聊 JavaScript
中一些常見的設(shè)計模式。
單例模式 (Singleton Pattern)
單例模式,顧名思義就是只有一個實例,并且它自己負(fù)責(zé)創(chuàng)建自己的對象,這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
代碼示例
// 單例模式
const Singleton = (function () {
let __instance = null;
const Singleton = function () {
if (__instance) return __instance;
__instance = this;
this.init();
return __instance;
}
Singleton.prototype.init = function () {
console.log('Singleton init completed!');
}
Singleton.getInstance = function () {
if (__instance) return __instance;
__instance = new Singleton()
return __instance;
}
return Singleton;
})();
const s1 = Singleton.getInstance();
const s2 = new Singleton();
console.log(s1 === s2); // true
利用 IIFE
的方式構(gòu)造 __instance
實例,并提供了獲取實例的方法,并保存起來,每次訪問時,如果已經(jīng)初始化則不需要重新創(chuàng)建,直接返回 __instance
即可,保證只有一個實例。
優(yōu)缺點
單例模式的優(yōu)點在于在創(chuàng)建后在內(nèi)存中只存在一個實例,能保證訪問一致性,并且反復(fù)創(chuàng)建銷毀實例來說節(jié)約內(nèi)存和 cpu
資源。
單例模式的缺點也很明顯,因為只有一個實例,也不太需要實例化過程,因此拓展性不好。
適用場景
比較適合項目中需要一個公共的狀態(tài),并通過單例也確保訪問一致性的時候。例如,許多 UI
框架 中的全局 Loading
組件。
工廠模式 (Factory Pattern)
工廠模式,聽名字就應(yīng)該可以大概猜到,是根據(jù)不同的輸入來創(chuàng)建同一類對象的。主要是為了將對象的創(chuàng)建與對象的實現(xiàn)分離。
代碼示例
// 工廠模式
/* 工廠類 */
function CreateElement(type) {
switch (type) {
case 'Input':
return new Input()
case 'DIV':
return new Div()
default:
throw new Error('當(dāng)前沒有這個產(chǎn)品')
}
}
/* 產(chǎn)品類 */
function Input() {
return document.createElement('input')
}
function Div() {
return document.createElement('div')
}
const input = new CreateElement('Input');
const div = new CreateElement('DIV');
console.log(input) // input
console.log(div) // div
上述通過一個簡單的例子演示了工廠模式的創(chuàng)建過程,我們知道有一個大工廠類,負(fù)責(zé)生產(chǎn)產(chǎn)品,只要通過傳遞不同的類型,可以實例化出不同的對象。
優(yōu)缺點
工程模式將對象的創(chuàng)建和實現(xiàn)進(jìn)行了分離,代碼結(jié)構(gòu)清晰,使得代碼高度解耦,具有很多良好的封裝,我們無需知道創(chuàng)建對象的過程就可以得到想要的對象,拓展性也強(qiáng)。
工廠模式的缺點在于,工廠類集中了所有實例的創(chuàng)建邏輯,它所能創(chuàng)建的類只能是事先考慮到的,如果需要添加新的類,則就需要改變工廠類。隨著產(chǎn)品類的不斷增多,代碼的維護(hù)性就越差。
適用場景
當(dāng)對象的創(chuàng)建比較復(fù)雜,而訪問者無需知道創(chuàng)建的具體流程,我們可以考慮使用工廠模式。典型應(yīng)用,如 Vue
、React
中創(chuàng)建虛擬 DOM
的 createElement
函數(shù)。Vue-Router
的設(shè)計,根據(jù)不同的 mode
創(chuàng)建不同的路由實例。
建造者模式(Builder Pattern)
建造者模式,將一個復(fù)雜對象的構(gòu)建與表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。在工程模式中,我們不關(guān)心創(chuàng)建過程,直接得到一個完成的對象。而建造者模式中,我們關(guān)心對象的創(chuàng)建過程,將復(fù)雜對象模塊化,使得每個模塊都可以復(fù)用。
代碼示例
// 建造者模式
/* 建造者 */
function ComputerBuilder(brand) {
this.brand = brand;
}
ComputerBuilder.prototype.buildCPU = function (type) {
switch (type) {
case 'inter':
this.cpu = 'inter 處理器';
break;
case 'AMD':
this.cpu = 'AMD 處理器';
break;
}
return this;
}
ComputerBuilder.prototype.buildMemory = function (mSize) {
this.mSize = '內(nèi)存' + mSize + 'G';
return this;
}
ComputerBuilder.prototype.buildDisk = function (dSize) {
this.dSize = '硬盤' + dSize + 'G';
return this;
}
/* 廠家,負(fù)責(zé)組裝 */
function computerDirector(brand, type, mSize, dSize) {
const _computer = new ComputerBuilder(brand);
_computer.buildCPU(type)
.buildMemory(mSize)
.buildDisk(dSize);
return _computer;
}
const com = computerDirector('聯(lián)想', 'inter', 16, 500);
console.log(com); // ComputerBuilder {brand: "聯(lián)想", cpu: "inter 處理器", mSize: "內(nèi)存16G", dSize: "硬盤500G"}
上述我們通過生產(chǎn)電腦的例子,描述了建造者模式的構(gòu)建過程,我們的部件都是由一個個類創(chuàng)建出來的,最后進(jìn)行組裝完成整個對象的。如果后期需要拓展組件,只需要在建造者上增加對應(yīng)的方法,再適當(dāng)修改鏈?zhǔn)秸{(diào)用即可。
優(yōu)缺點
建造者模式適用于構(gòu)建復(fù)雜的、需要分步驟構(gòu)建的對象,可以將構(gòu)建過程分離,分步驟進(jìn)行。優(yōu)點顯而易見,具有很好的拓展性,很高的復(fù)用性。
如果對象之間差異過大,復(fù)用性不高的話不建議使用這種模式,否則創(chuàng)建過程中會導(dǎo)致代碼比較亂,復(fù)雜度過高,顯得有些強(qiáng)行建造了。
適用場景
建造者模式適用于可以通過不同的部件組裝得到不同完整產(chǎn)品的場景,可以將代碼最小程度的拆分,利于后期維護(hù)。例如,你封裝一個公共彈窗,里面涉及有標(biāo)題,內(nèi)容,按鈕,文字等,但也不都是必須的,你可以在需要的時候去構(gòu)建他們。
代理模式(Proxy Pattern)
代理模式,是給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用,防止訪問者直接訪問目標(biāo)對象,從而對目標(biāo)對象起到一種間接保護(hù)的作用,類似我們?nèi)粘I钪械母鞣N中介及微商。
ps
: ES6
新增的 Proxy
可以非常方面快捷的幫助我們對一個對象進(jìn)行代理,想要了解 Proxy
的同學(xué)可參考我之前的寫的 初探 Vue3.0 中的一大亮點——Proxy !
代碼示例
// 代理模式
// 目標(biāo)
function sendMsg(msg) {
console.log(msg);
}
// 代理
function ProxyMsg(msg) {
if (!msg) {
console.log('msg is empty.')
return
}
msg = '我要發(fā)送的數(shù)據(jù)是:' + msg;
sendMsg(msg);
}
ProxyMsg('您好!'); // 我要發(fā)送的數(shù)據(jù)是:您好!
上述用一個發(fā)送消息的例子來描述了代理模式的工作原理,我們不直接通過 sendMsg
方法而是通過 ProxyMsg
方法來發(fā)送消息。這樣做的好處是可以在發(fā)送消息之前對一些不合法的消息進(jìn)行過濾,對合法的內(nèi)容進(jìn)行二次包裝。
優(yōu)缺點
代理模式的優(yōu)點在于,代理對象作為訪問者與目標(biāo)對象之間橋梁,對目標(biāo)對象起到保護(hù)的作用。可以很方面的拓展代理對象,并不直接干涉目標(biāo)對象,一定程度上降低了系統(tǒng)的耦合度。
另一方面,額外新增的代理對象無異于增加了整個系統(tǒng)的復(fù)雜度,造成請求處理速度變慢,增加了系統(tǒng)的維護(hù)成本,在使用前需要酌情考慮。
適用場景
隨著前端的不斷發(fā)展,代理模式在前端領(lǐng)域的使用場景還是很多的。
典型的應(yīng)用就是攔截器,項目中 axios
數(shù)據(jù)請求中的 interceptor
攔截器,以及一些權(quán)限校驗的中間件等。另外像目前流行的 vue
框架,其中的數(shù)據(jù)響應(yīng)式就是利用了這一思想,不同的是 vue2
采用的是 Object.defineProperty
, 而 vue3
采用的是 Proxy
。
享元模式(Flyweight Pattern)
享元模式,字面解釋,享就是共享,元就是元素,公共部分。
因此,享元模式就是通過共享技術(shù)實現(xiàn)相同或相似對象的重用,主要用于減少創(chuàng)建對象的數(shù)量,以減少內(nèi)存占用和提高性能。
代碼示例
// 享元對象
function Shape(shape) {
this.shape = shape;
}
Shape.prototype.draw = function () {
console.log(`畫了一個 ${this.shape}`)
}
// 享員工廠
const ShapeFactory = (function () {
const dataMap = {};
return {
getShapeContext(shape) {
// 如果存在,則直接返回
if (dataMap[shape]) return dataMap[shape];
else {
// 沒有就創(chuàng)建,并保存當(dāng)前shape的實例
const instance = new Shape(shape);
dataMap[shape] = instance
return instance;
}
}
}
})();
const rect = ShapeFactory.getShapeContext('rect');
const circle = ShapeFactory.getShapeContext('circle');
rect.draw(); // 畫了一個 rect
circle.draw(); // 畫了一個 circle
上述代碼,我們用來一個繪畫的例子,通過享元工廠去創(chuàng)建不同類型的 "畫筆" 對象,并保存在我們的工廠函數(shù)中,下次使用的時候則不需要重新創(chuàng)建,直接從 map
中讀取即可。這種方式,相比于傳統(tǒng)的用的時候去 new
創(chuàng)建在數(shù)據(jù)量大的時候會節(jié)約很多內(nèi)存。
優(yōu)缺點
享元模式最大的優(yōu)點就在于它可以極大的減少了系統(tǒng)中對象的創(chuàng)建,降低內(nèi)存的使用,加快了運(yùn)行速度,提高了運(yùn)行效率。
提高效率的同時也暴露出其缺點,共享對象的創(chuàng)建,銷毀等都需要增加額外的邏輯,會使整個系統(tǒng)的邏輯變得復(fù)雜,代碼不容易閱讀,維護(hù)的成本增加。
適用場景
享元模式比較適合項目中大量使用了相同或相似對象,可以共享資源時可以考慮。
其實在前端開發(fā)設(shè)計中還是比較常見的,例如我們所熟知和使用的 事件委托 經(jīng)行事件綁定,就是利用了享元模式的原理,我們并不是給每個元素綁定事件,而是為其父元素綁定一個事件,根據(jù)事件參數(shù) event
來判斷。
另外 nodejs
中所使用的數(shù)據(jù)庫連接池,一些緩存服務(wù)器的設(shè)計等是利用這個原理。
適配器模式(Adapter Pattern)
適配器模式,作為兩個不兼容的接口之間的橋梁,目的就是通過適配器的轉(zhuǎn)換解決類(對象)之間接口不兼容的問題,從而使得原本不兼容的接口可以兼容現(xiàn)有的需求。
與早些年傳統(tǒng)的萬能充電器的作用類似。
代碼示例
// 適配器模式
// 百度地圖 api
const baiduMap = {
show: function () {
console.log('開始渲染百度地圖')
}
}
// 高德地圖 api
const AMap = {
render: function () {
console.log('開始渲染高德地圖')
}
}
// 適配器
const baiduAdapter = {
render: function () {
return baiduMap.show()
}
}
function renderMap(map) {
if (typeof map.render === 'function') {
map.render()
}
}
renderMap(AMap); // 開始渲染高德地圖
renderMap(baiduAdapter); // 開始渲染百度地圖
上述代碼中演示了適配器模式的原理,我們之前用的是高德地圖 ,如今我們也要接入百度地圖,二者的 api
的渲染方式不同,為了解決不兼容的問題,我們構(gòu)造了一個 baiduAdapter
適配器,這樣我們就可以適用同樣的接口完成不同地圖的渲染。
優(yōu)缺點
適配器模式相對來說是一種簡單的設(shè)計模式,目的就是為了兼容舊的代碼。因此,它的優(yōu)點也很明顯,就是不用大面積更改以前的舊的代碼邏輯,使得原有的邏輯可以復(fù)用,拓展性強(qiáng),靈活性強(qiáng),可以隨時隨地更改或刪除不同的適配器,而不會造成重大的影響。
適配器模式的缺點自然而然就是,多的適配器會增加系統(tǒng)的復(fù)雜度,會使得系統(tǒng)的代碼變得十分松散,凌亂,代碼的的可閱讀性大大折扣。如大規(guī)模使用適配器導(dǎo)致代碼變得凌亂松散時,可以考慮重構(gòu)。
適用場景
適配器模式有點 亡羊補(bǔ)牢 的意思,如果現(xiàn)有的接口已經(jīng)能夠正常工作,那就永遠(yuǎn)不會用上適配器模式。但隨著公司業(yè)務(wù)的發(fā)展,也許現(xiàn)在好好工作的接口,未來的某天卻不再適用于新系統(tǒng),這時候就得考慮適用適配器模式,為其重新賦能。
裝飾器模式 (Decorator Pattern)
裝飾器模式,在不改變原對象的基礎(chǔ)上,對其添加屬性或方法來進(jìn)行拓展,使原有對象可以具有更多功能,而不影響原有的對象結(jié)構(gòu)。與繼承相比裝飾者是一種更輕便靈活的做法。
ps
:ES7
關(guān)于裝飾器 @Decorator
已經(jīng)在草案中,我們項目中所使用的裝飾器需借助第三方工具轉(zhuǎn)義,等到時候標(biāo)準(zhǔn)定下來后就可放心使用了。
代碼示例
const btn = document.querySelector('#btn');
// 原綁定事件
btn.onclick = function () {
console.log('按鈕被點擊了')
}
// 新增統(tǒng)計
function ajaxToServer() {
console.log('數(shù)據(jù)統(tǒng)計')
}
// 裝飾器函數(shù)
function decorator(target, eventName, cb) {
const originFn = target['on' + eventName];
originFn && originFn()
cb && cb();
}
decorator(btn, 'click', ajaxToServer)
以一點按鈕的點擊事件為例,原點擊事件只是打印 按鈕被點擊了
,現(xiàn)在需要再點擊的時候調(diào)用 ajaxToServer
做數(shù)據(jù)統(tǒng)計,我們通過一個 decorator
裝飾器函數(shù)先將原始綁定的事件緩存起來,再添加我們 ajaxToServer
回調(diào)即可,一來沒有影響到原始代碼的改動,二來后期如果需要新增,按部就班即可。
優(yōu)缺點
裝飾器的有點在于我們不需要關(guān)心原對象的實現(xiàn),裝飾者和被裝飾者之間不會相互耦合,就可以拓展原對象的方法,可維護(hù)性好,并且裝飾器還可以復(fù)用,使得對象的拓展更加靈活。
由于裝飾器的靈活性,因此隨著裝飾器的增多,會導(dǎo)致系統(tǒng)復(fù)雜度增加,尤其是多級裝飾器,會導(dǎo)致代碼錯誤定位困難繁瑣,對于不熟悉這個模式的開發(fā)人員難以理解。
適用場景
裝飾器模式適用于需要動態(tài)拓展對象或類的方法,或者需要對一些功能進(jìn)行排列組合,完成復(fù)雜工功能的時候可以考慮裝飾器模式。
我們在使用vue
,或者scss
的時候,有時候會用到 mixins
,mixins
的原理就類似于裝飾器。
外觀模式 (Facade Pattern)
外觀模式的本質(zhì)是封裝交互,簡化調(diào)用,它的做法是隱藏了系統(tǒng)的復(fù)雜性,將子系統(tǒng)的一組接口封裝起來,給使用者提供了一個統(tǒng)一的高層接口,減少了客戶端與子系統(tǒng)之間的耦合性。
在JavaScript
中外觀模式常常用于解決瀏覽器兼容性問題以及源碼中的一些函數(shù)重載,很多主流的庫,如 jQuery
,lodash
等都有涉及。
代碼示例
// 事件綁定
function addEvent(element, type, fn) {
if (element.addEventListener) { // 支持 DOM2 級事件處理方法的瀏覽器
element.addEventListener(type, fn, false)
} else if (element.attachEvent) { // 不支持 DOM2 級但支持 attachEvent
element.attachEvent('on' + type, fn)
} else {
element['on' + type] = fn // 都不支持的瀏覽器
}
}
// 阻止事件冒泡
function cancelBubble(event) {
if (event.stopPropagation) {
event.stopPropagation()
} else { // IE 下
event.cancelBubble = true
}
}
// axios 中 getDefaultAdapter
function getDefaultAdapter() {
var adapter;
// Only Node.JS has a process variable that is of [[Class]] process
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
上述的代碼片段都是演示了外觀模式的特點,對于用戶端來說都是統(tǒng)一的調(diào)用,但是接口內(nèi)部卻根據(jù)傳參不同,或者運(yùn)行環(huán)境不同等做了對應(yīng)的處理,簡化了客戶端的使用。
優(yōu)缺點
外觀模式的優(yōu)點在于使用者不必關(guān)系子系統(tǒng)的具體實現(xiàn),通過統(tǒng)一的接口調(diào)用就能達(dá)到效果,降低了使用者和系統(tǒng)模塊之間的耦合性,增加了可維護(hù)性和可擴(kuò)展性。
由于外觀模式是將一組子系統(tǒng)的接口進(jìn)行整合,所以它的缺點就很明顯,在系統(tǒng)內(nèi)部擴(kuò)展子系統(tǒng)時 , 容易產(chǎn)生風(fēng)險。
適用場景
外觀模式在很多開源作品中屢見不鮮,例如上述提到的 jQuery
,lodash
等,實際上我們開發(fā)也會經(jīng)常用到。它比較適合將復(fù)雜的系統(tǒng)進(jìn)行分層,讓外觀模塊成為每層的入口,簡化層與層之間調(diào)用。或者說當(dāng)我們需要通過一個單獨(dú)的函數(shù)或方法來訪問一系列的函數(shù)或方法調(diào)用時,為了使代碼更容易跟蹤管理或者更好的維護(hù)時,可以考慮適用外觀模式。
組合模式 (Composite Pattern)
組合模式是將一系列對象組合成樹形結(jié)構(gòu),以表示 “部分-整體” 的層次結(jié)構(gòu),使用者只需統(tǒng)一地使用組合結(jié)構(gòu)中的所有對象,而不需要關(guān)心它究竟是組合對象還是單個對象。它主要體現(xiàn)了整體與部分的關(guān)系,其典型的應(yīng)用就是樹形結(jié)構(gòu)。
代碼示例
// 創(chuàng)建部門
function createApartment(name) {
return {
name,
_children: [],
add(target) {
this._children.push(target);
return this;
},
show(cb) {
this._children.forEach(function (child) {
child.show(cb)
})
}
}
}
// 創(chuàng)建員工
function createEmp(num, name) {
return {
num,
name,
show(cb) {
cb(this)
}
}
}
// 創(chuàng)建部門
const techApartment = createApartment('技術(shù)部');
// 創(chuàng)建子部門
const proApartment = createApartment('產(chǎn)品組'),
devApartment = createApartment('開發(fā)組');
techApartment.add(proApartment).add(devApartment);
proApartment.add(createEmp(100, '張三'))
.add(createEmp(101, '李四'))
techApartment.add(createEmp(201, '小劉'))
.add(createEmp(202, '小王'))
.add(createEmp(203, '小陳'))
.add(createEmp(204, '小亮'))
// 遍歷
techApartment.show(function (item) {
console.log(`工號:${item.num},姓名:${item.name}`)
})
/***
工號:100,姓名:張三
工號:101,姓名:李四
工號:201,姓名:小劉
工號:202,姓名:小王
工號:203,姓名:小陳
工號:204,姓名:小亮
***/
上述我們同通過一個部門的組織架構(gòu)圖來展示了什么是組合模式,可以發(fā)現(xiàn)組合對象和單個子對象具有相同的接口和數(shù)據(jù)結(jié)構(gòu),一次來保證操作一致,我們在遍歷整個 techApartment
對象時,如果當(dāng)前對象是沒有子對象,則自身會做處理,否則會傳遞到下一個子對象中處理,以此完成整個遞歸遍歷。
優(yōu)缺點
組合模式的組合對象和單個子對象具有同樣的接口,所以無論調(diào)用的是組合對象還是葉子對象調(diào)用方式上沒有差別,外部調(diào)用非常方便。拓展性良好,新增節(jié)點會很方便,也不會影響到其他的對象。
隨著節(jié)點的增加,組合模式也暴露出其不足,過多的節(jié)點會導(dǎo)致整個樹狀結(jié)構(gòu)非常復(fù)制,層級嵌套深,內(nèi)存占用較高,導(dǎo)致系統(tǒng)整體性能下降。
適用場景
如果對象組織呈樹形結(jié)構(gòu),操作樹中對象的方法比較類似時可以考慮適用組合模式。常見的比如組織架構(gòu)圖,文件目錄,以及熟悉的 vue
中的 createElement
方法等都采用的組合模式這種設(shè)計理念。
橋接模式(Bridge Pattern)
橋接模式是為了將抽象部分與實現(xiàn)部分分離,使抽象部分和實現(xiàn)部分都可以獨(dú)立的變化而不會互相影響,實現(xiàn)二者的解耦,從而降低了代碼的耦合性,提高了代碼的擴(kuò)展性。
代碼示例
// 橋接方法
function addEvent(ele, eventName, fn) {
document.querySelector(ele).addEventListener(eventName, fn, false);
}
// 具體業(yè)務(wù)
addEvent('#btn', 'click', function () {
console.log('hello world'); // hello world
})
上述通過一個簡單的事件監(jiān)聽器的例子來展示了橋接模式的工作原理,橋接方法 addEvent
它內(nèi)部不實現(xiàn)具體的業(yè)務(wù)邏輯,只是抽象出一個方法,它就充當(dāng)了了 DOM
元素與其具體事件綁定的一個橋梁,要實現(xiàn)具體的業(yè)務(wù)邏輯只要給橋接函數(shù)傳遞參數(shù)即可。
優(yōu)缺點
橋接模式分離了抽象和實現(xiàn)部分,將實現(xiàn)層(DOM
元素事件具體邏輯)和抽象層(綁定方法)解耦,使用者不需要關(guān)心細(xì)節(jié)的實現(xiàn),只需要方便快捷的使用即可,提高了代碼的拓展性。
橋接模式的弊端在于需要很好地抽象出橋接方法與業(yè)務(wù)邏輯,具有一定的局限性,另外橋接模式會引入額外的代碼,增加系統(tǒng)的復(fù)雜度。
適用場景
如果開發(fā)中遇到部分系統(tǒng)的復(fù)用性大,且各個部件有獨(dú)立的變化維度,就可以考慮引入橋接模式,實現(xiàn)代碼的分層。常見的如同上述的事件監(jiān)聽器,動態(tài)更新 dom
的樣式,以及 ajax
請求封裝等。
發(fā)布-訂閱模式 (Publish-Subscribe Pattern)
發(fā)布-訂閱模式,它定義了一種一對多的關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知,使得它們能夠自動更新。
代碼示例
// 事件監(jiān)聽器
const Emitter = (function () {
const _events = {};
return {
// 事件綁定
on(type, cb) {
if (!_events[type]) {
_events[type] = [];
}
if (typeof cb === "function") {
_events[type].push(cb);
} else {
throw new Error('參數(shù)類型必須為函數(shù)')
}
},
// 事件解綁
off(type, cb) {
if (!_events[type] || !_events[type].includes(cb)) return;
// 移除事件監(jiān)聽
_events[type].map((fn, index) => {
if (fn === cb) {
_events[type].splice(index, 1)
}
})
},
emit(type, ...args) {
if (!_events[type]) return;
_events[type].forEach(cb => cb(...args))
}
}
})();
// 事件訂閱
Emitter.on('change', data => console.log(`我是第一條信息:${data}`))
Emitter.on('change', data => console.log(`我是第二條信息:${data}`))
// 事件發(fā)布
Emitter.emit('change', '參數(shù)')
上述我們通過發(fā)布訂閱模式實現(xiàn)了一個簡單的事件監(jiān)聽器,可以通過 on
方法監(jiān)聽某一事件,之后通過 emit
方法去分發(fā),所有監(jiān)聽該事件的函數(shù)都會被依次執(zhí)行,這就是發(fā)布訂閱模式基本工作原理。
優(yōu)缺點
發(fā)布訂閱模式最大的特點就是發(fā)布者和訂閱者之間完全解耦:發(fā)布者不需要訂閱者是誰,只需要更新的時候遍歷所以訂閱該消息的訂閱者即可。訂閱者也不需要時時關(guān)注發(fā)布者的動態(tài),當(dāng)有消息更新時會自動接受。因此,可以將事件處理中心封裝起來,統(tǒng)一管理,獨(dú)立運(yùn)行。
發(fā)布訂閱模式的缺點在于,訂閱者會增加內(nèi)存消耗,及時后續(xù)沒有觸發(fā),也會常駐內(nèi)存中。隨著訂閱者的增多,系統(tǒng)復(fù)雜度會增加,代碼運(yùn)行效率、資源消耗會變大。另外,發(fā)布者與訂閱者完全解耦,會導(dǎo)致代碼追蹤起來比較困難。
適用場景
發(fā)布訂閱模式特別適用于要實現(xiàn)一對多關(guān)聯(lián)的場景。日常生活中我們訂閱的公眾號,關(guān)注的明星微博,今日頭條的新聞等,他們都會在有新消息的時候第一時間推送給你。而實際開發(fā)中,vue
的數(shù)據(jù)響應(yīng)式,瀏覽器的 DOM
事件綁定等也都是這個原理。
策略模式 (Strategy Pattern)
策略模式就是將一系列算法封裝起來,并使它們相互之間可以替換。被封裝起來的算法具有獨(dú)立性,外部不可改變其特性。它的目的就是將算法的使用與算法的實現(xiàn)分離開來,有效避免代碼中很多if-else
的條件語句。
代碼示例
// 校驗規(guī)則
const strategyMap = {
// 校驗手機(jī)號
isMobile(mobile) {
return /^1\d{10}$/.test(mobile);
},
// 校驗是否必填
isRequired(str) {
return str.replace(/(^\s*)|(\s*$)/g, "") !== "";
}
};
// 校驗方法
function validate(formData) {
let valid;
for (let key in formData) {
const val = formData[key].value;
const rules = formData[key].rules;
for (let i = 0; i < rules.length; i++) {
const result = strategyMap[rules[i]['rule']].call(null, val);
if (!result) {
valid = {
errField: key,
errValue: val,
errMsg: rules[i]['message']
}
break;
}
}
if (valid) return valid;
}
return valid;
}
// form 表單校驗
const formData = {
mobile: {
value: '1380000000',
rules: [
{rule: 'isRequired', message: '手機(jī)號碼不能為空'},
{rule: 'isMobile', message: '手機(jī)號碼格式不正確'},
]
}
}
// 獲取校驗結(jié)果
const valid = validate(formData)
if (!valid) {
console.log('校驗通過')
} else {
console.log(valid)
// {errField: "mobile", errValue: "1380000000", errMsg: "手機(jī)號碼格式不正確"}
}
上述用了一個非常經(jīng)典的表單校驗來展示了策略模式的應(yīng)用,我們可以事先將一些校驗規(guī)則即一些策略算法放到一個 map
中,通過 validate
方法來完成對復(fù)雜表單的校驗,相比于傳統(tǒng)的 if-else
判斷看起來簡單明了的多,也可以大大提高開發(fā)效率。
優(yōu)缺點
通過上述例子,可以發(fā)現(xiàn)策略模式可以將一個個算法封裝起來,提高代碼復(fù)用率,減少代碼冗余;它可看作為 if/else
判斷的另一種表現(xiàn)形式,在達(dá)到相同目的的同時,極大的減少了代碼量以及代碼維護(hù)成本。另外策略模式中各個策略之間相互獨(dú)立,互不影響,使得它具有良好的可擴(kuò)展性。
策略模式的缺點在于各個策略相互獨(dú)立,因此一些復(fù)雜的算法邏輯無法共享,造成資源的浪費(fèi)。另一方面,我們必須實現(xiàn)定義好種種策略,且使用者必須事先了解這些策略方能靈活運(yùn)用,在一定程度上來講,對于使用者來說不是很方便。
適用場景
策略模式比較適合實現(xiàn)某一個功能有多種方案可以選擇,自由切換的場景,或者是有時需要多重條件判斷,可以使用策略模式來規(guī)避多重條件判斷的情況。前端典型應(yīng)用就是許多開源框架中 form
表單的動態(tài)校驗,以及電商系統(tǒng)中不同優(yōu)惠券的對應(yīng)不同的邏輯等。
狀態(tài)模式 (State Pattern)
狀態(tài)模式定義是一個對象在其內(nèi)部狀態(tài)改變時對應(yīng)的改變它的行為,對象看起來似乎修改了它的類。其意思就是說 對象行為是基于狀態(tài)來改變的,內(nèi)部的狀態(tài)轉(zhuǎn)化,導(dǎo)致了行為表現(xiàn)形式不同。
其主要是用來解決系統(tǒng)中復(fù)雜對象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題。
代碼示例
// 正常狀態(tài)
function NormalState() {
this.handleChange = function (context) {
console.log('正常狀態(tài)')
context.state = new ColorfulState()
}
}
// 彩燈狀態(tài)
function ColorfulState() {
this.handleChange = function (context) {
console.log('彩燈狀態(tài)')
context.state = new CloseState()
}
}
// 關(guān)閉狀態(tài)
function CloseState() {
this.handleChange = function (context) {
console.log('關(guān)閉狀態(tài)')
context.state = new NormalState()
}
}
// 燈
function Light(state) {
this.state = state;
this.switch = function () {
this.state.handleChange(this)
}
}
// 設(shè)置燈光初始為關(guān)閉
const light = new Light(new CloseState());
setInterval(() => {
light.switch()
}, 1000)
// 關(guān)閉狀態(tài)-->正常狀態(tài)-->彩燈狀態(tài)-->關(guān)閉狀態(tài)...
我們通過生活中一個客廳的燈光狀態(tài)來掩飾狀態(tài)模式的是如何工作的,我們把每個狀態(tài)定義成一個類,并且把每個狀態(tài)所對應(yīng)的功能處理封裝起來,這樣選擇不同狀態(tài)的時候,其實就是在選擇不同的狀態(tài)處理類,由于狀態(tài)是在運(yùn)行期被改變的,因此行為也會在運(yùn)行期根據(jù)狀態(tài)的改變而改變,看起來,同一個對象,在不同的運(yùn)行時刻,行為是不一樣的,就像是類被修改了一樣。
優(yōu)缺點
很明顯,狀態(tài)模式之間的狀態(tài)都是一個個不同的類,相比于 switch-case
或 if-else
語句的使用,狀態(tài)模式結(jié)構(gòu)很清晰,拓展性很好,需要添加狀態(tài)時,只需要在新增一個狀態(tài)類即可,并且狀態(tài)切換提供了統(tǒng)一的接口,外部的調(diào)用無需知道類內(nèi)部如何實現(xiàn)狀態(tài)和行為的變換,具有很好的封裝性。
狀態(tài)模式的缺點在于,每個狀態(tài)都有對應(yīng)的類,因此系統(tǒng)會引入了很多的類,這樣導(dǎo)致系統(tǒng)中類的個數(shù)增加,維護(hù)成本變高。
適用場景
如果系統(tǒng)的代碼中有多分支的條件語句,且這些分支依賴于某個對象的狀態(tài)時,可以考慮使用狀態(tài)模式來將分支的處理分散到單獨(dú)的狀態(tài)類中,來實現(xiàn)狀態(tài)和行為的分離。
前端的Promise
就是一個典型的狀態(tài)模式。前端處理 ajax
請求返回不同的 status
時對應(yīng)的處理邏輯,就可以考慮使用狀態(tài)模式了。
命令模式 (Command Pattern)
命令模式,就是將一系列操作的指令封裝起來,根據(jù)客戶端不同的請求參數(shù)執(zhí)行的對應(yīng)的方法,本質(zhì)上是對方法調(diào)用的封裝,但它可以使請求發(fā)送者和接收者消除彼此之間的耦合關(guān)系。
代碼示例
const Manager = (function () {
// 命令
const commander = {
open: function () {
console.log('打開電視')
},
close: function () {
console.log('關(guān)閉電視')
},
change: function (channel) {
console.log('更換頻道 ' + channel)
}
}
return {
// 執(zhí)行命令
exec: function (cmd) {
const args = [].splice.call(arguments, 1)
commander[cmd] && commander[cmd](args)
}
}
})();
Manager.exec('open') // 打開電視
Manager.exec('change', 10) // 更換頻道 10
Manager.exec('close') // 關(guān)閉電視
上述代碼以一種簡單的方式展示了命令模式的基本用法,我們是先定義好一些命令,并暴露出一個執(zhí)行命令的 exec
方法,使用者就可以通過 Manager.exec
傳遞不同的命令參數(shù)來達(dá)到執(zhí)行不同命令的效果。
優(yōu)缺點
上面的代碼很明顯就可以看出,命令模式中命令的請求和命令的執(zhí)行兩者完全解耦,因此系統(tǒng)的可擴(kuò)展性良好,加入新的命令不會影響原有邏輯,而且復(fù)用性很強(qiáng),可以被任何請求者使用,不關(guān)心請求者是誰。
命令模式的缺點在于,一是使用者要事先了解有哪些命令方能正常使用,二是隨著命令的不斷增加系統(tǒng)會變得很膨脹,復(fù)雜性會隨之增加。
適用場景
命令模式比較適合于需要發(fā)布一些命令,但不清楚接受者和請求的操作,即只用知道發(fā)布了一個指令就行,具體做什么誰來做不用關(guān)心。常見的 GUI
編程中基本都采用這種模式,前端比較典型應(yīng)用如,富文本編輯器中的各種按鈕,canvas
動畫中各種指令操作等。
總結(jié)
我們上面用了很大的篇幅總結(jié)了常見的一些的設(shè)計模式在 JavsScript
中的實現(xiàn),雖然在 js
中有些設(shè)計模式看起來有些不盡人意,但這卻不是我們所要關(guān)注的核心,我們真正需要關(guān)心的是這些設(shè)計模式的理念、它所要解決的問題。
設(shè)計模式對于我們學(xué)習(xí)框架源碼,做一些前端架構(gòu)是非常有幫助的,只有真正了解了它的思想,明白它所能解決的問題,才能讓我們在開發(fā)中少走彎路,寫出高質(zhì)量的代碼。
也希望閱讀到這的你,繼續(xù)加油,時刻保持一顆學(xué)習(xí)的心態(tài),繼續(xù)在程序員這條道路上摸爬滾打!