從JavaScript中看設計模式(總結)


從JavaScript中看設計模式(總結)


概念

設計模式 (Design Pattern) 是一套被反覆使用、多數人知曉的、經過分類的、代碼設計經驗的總結。

任何事情都有套路,設計模式就是寫代碼中常見的套路,有些寫法我們日常都在使用,下面我們來介紹一下。

訂閱/發佈模式(觀察者)

pub/sub這個應該大家用到的最廣的設計 模式了

在這種模式中,並不是一個對象調用另一個對象的方法,而是一個對象訂閱另一個對象

特定活動並在狀態改變後獲得通知,訂閱者因此也成為觀察者,而被觀察的對象成為發佈者或主題。當發生了一個重要事件的時候發佈者會通知(調用)所有訂閱者並且可能經常以事件對象的形式傳遞消息。

自己實現一個簡單的發佈訂閱設計模式

<code>// 創建EventBus
class EventBus {
constructor() {

// 儲存事件
this.tasks = {};
}
// 綁定事件
$on(eName, cb) {
typeof cb == "function"
? this.tasks[eName] || (this.tasks[eName] = [])
: this.Error(cb, "is not a function");
this.tasks[eName].some(fn => fn == cb)
? true
: this.tasks[eName].push(cb); // 避免重複綁定
}
// 觸發事件
$emit(eName, ...arg) {
let taskQueue;
this.tasks[eName] && this.tasks[eName].length > 0
? (taskQueue = this.tasks[eName])
: this.Error(eName, "is not defined or is a array of having empty callback");
taskQueue.forEach(fn => {
fn(...arg);
});
}
// 觸發一次
$once(eName, cb) {
let fn = (...arg) => {
this.$off(eName, fn);
cb(...arg);
};
typeof cb == "function" && this.$on(eName, fn);
}
// 卸載事件
$off(eName, cb) {
let taskQueue;
this.tasks[eName] && this.tasks[eName].length > 0
? (taskQueue = this.tasks[eName])
: this.Error(eName, "is not exist");
if (typeof cb === "function") {
let index = taskQueue.findIndex(v => (v == cb));
index != -1 &&
taskQueue.splice(
taskQueue.findIndex(v => (v == cb)),
1
);
}
if (typeof cb === "undefined") {
taskQueue.length = 0;
}
}

// 異常處理
Error(node, errorMsg) {
throw Error(`${node} ${errorMsg}`);
}
}
複製代碼/<code>

下面我們針對自己的模式進行簡單的使用:

<code>// 首先定義一個事件池
const EventSinks = {
add(x, y) {
console.log("總和: " + x + y);
},
multip(x, y) {
console.log("乘積: " + x * y);
},
onceEvent() {
console.log("我執行一次後就自動卸載");
}
};

// 實例化對象
let bus = new EventBus();
bus.$on("operator", EventSinks.add); // 監聽operator事件, 增加一個EventSinks.add
bus.$on("operator", EventSinks.add); // 當事件名和回調函數相同時,跳過,避免重複綁定
bus.$on("operator", EventSinks.multip); // 給operator事件增加一個EventSinks.multip回調函數
bus.$once("onceEvent", EventSinks.onceEvent); // 觸發一次後卸載
console.log(bus.tasks); // { operator: [ [Function: add], [Function: multip] ], onceEvent: [ [Function: fn] ]}
bus.$emit("operator", 3, 5); // 總和:8 乘積:15
bus.$emit("onceEvent"); // 我就執行一次
console.log(bus.tasks); // { operator: [ [Function: add], [Function: multip] ], onceEvent: [] }
bus.$off("operator", EventSinks.add); // 卸載掉operator事件中的EventSinks.add函數體
console.log(bus.tasks); // { operator: [ [Function: multip] ], onceEvent: [] }
bus.$off("operator"); // 卸載operator事件的所有回調函數
console.log(bus.tasks); // { operator: [], onceEvent: [] }
bus.$emit("onceEvent"); // onceEvent is not defined or is a array of having empty callback

複製代碼/<code>

單例模式

單例模式的定義:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。實現的方法為先判斷實例存在與否,如果存在則直接返回,否則就創建實例再返回,這就保證了一個類只實例化一次

使用場景:一個單一對象。比如:彈窗,無論點擊多少次,彈窗只應該被創建一次,實現起來也很簡單,用一個變量緩存起來即可。可以參考ElementUI模態框的實現

模仿一下單例模式(只要有個變量確保實例只創建一次)

<code>class Singleton {
constructor() {}
}

Singleton.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
複製代碼/<code>

當我們再次創建時,如果實例化了,就不在實例化,直接返回,上面可以看出,二者相同

策略模式

策略模式的定義:定義一系列的算法,把他們一個個封裝起來,並且使他們可以互相替換

策略模式的目的就是將算法的使用算法的實現分離出來

一個基於策略模式的程序至少由兩部分組成。第一部分是一組策略類(可變),策略類封裝了具體的算法,並負責具體的計算過程。第二部分是環境類Context(不變),Context接受客戶的請求,隨後將請求委託給某一個策略類。要做到這一點,說明Context中要維持對某個策略對象的引用

舉個表單校驗栗子:

<code>// 普通寫法
const form = document.querySelector("#form");
form.onsubmit = () => {
if (form.username.value == "") {
console.log("用戶名不能為空");
return false;
}
if(form.username.password.length < 10){
console.log('密碼長度不能小於10')
return false
}
}
複製代碼/<code>

簡單的策略模式

<code>// 創建校驗器 

const checker = {
isEmpty(v, errorMsg){
if(value === ''){
return errorMsg
}
},
minLength(v, length, errorMsg){
if(value.length < length){
return errorMsg
}
}
}
const validator = () => {
// 校驗規則存儲
this.cache = []
}
validator.prototype.add = (...rule) => {
let arr = rule.split(',')
this.cache.push(() => {
let valit = arr.shift()
arr.unshift(dom.value)
arr.push(errorMsg)
return checker[valit].apply(dom, arr)
})
}
validator.prototype.start = () => {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];){
// 開始校驗,並取得校驗後的返回值
let msg = validatorFunc()
if(msg){
return msg
}
}
}
const validatorFunc = () => {
// 創建一個validator對象
let valit = new validator()
valit.add(form.username, 'isEmpty', '用戶名不能為空')
valit.add(form.password, 'minLength', '密碼長度不能小於10')
// 獲得校驗結果
let errorMsg = valit.start()
return errorMsg
}

// 再次登錄

const form = document.querySelector("#form");
form.onsubmit = () => {
let errorMsg = validatorFunc()
if(errorMsg){
console.error(errorMsg)
return false
}
}
複製代碼/<code>

當創建校驗器後,校驗規則清晰明瞭,可以動態增改,便於維護

代理模式

代理模式的定義:為一個對象提供一個代用品或佔位符,以便控制它的訪問

常用的虛擬代理形式:某一個花銷很大的操作,可以通過虛擬代理的方式延遲這種需要他的時候才去創建(例:使用虛擬代理實現圖片懶加載)

圖片懶加載的方式:先通過一張loading圖佔位,然後通過異步的方式加載圖片,等圖片加載好了再把請求成功的圖片加載到img標籤上

栗子:

<code>const imgFunc = (() => {
const imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return{
setSrc: function(src){
imgNode.src = src
}
}
})()
const proxyImage = (() => {

let img = new Image()
img.onload = function(){
imgFunc.setSrc(this.src)
}
return {
setSrc: function(src){
imgFunc.setSrc('./loading.gif')
img.src = src
}
}
})()
proxyImage.setSrc('./pic.png')()
複製代碼/<code>

上面的栗子實現了加載圖片時,在圖片加載成功前,指定特定的圖片,加載完成後替換成真是的數據

在我們生活中常用的事件代理、節流防抖函數其實都是代理模式的實現

裝飾器模式

裝飾器模式的定義:在不改變對象自身的基礎上,在程序運行期間給對象動態地添加方法,註解也可以理解為裝飾器。常見應用:react的高階組件,或者react-redux中的@connect或者自己定義一些高階組件

簡單實現:

<code>import React from 'react'
const withLog = Component => {
// 完好無損渲染出來, 只是添加了兩個生命週期函數
class NewComponent extends React.Component{
// 1
componentWillMount(){
console.time('ComponentRender')
console.log('準備完畢了')

}
render(){ // 完好無損渲染出來
return <component>
}
// 2
componentDidMount(){
console.timeEnd('ComponentRender')
console.log('渲染完畢了')
}
}
return NewComponent
}
export { withLog }

@withLog
class xxx
複製代碼/<code>

在redux中可以找出裝飾器的方式,其實Vue中的v-input,v-checkbox也可以認為是裝飾器模式,對原生input和checkbox做一層裝飾

裝飾器模式和代理模式的結構看起來非常相似,這兩種模式都描述了怎樣為對象提供一定程度上的間接引用,並且向那個對象發送請求。代理模式和裝飾器模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是:當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。裝飾模式目的是:為對象動態加入的行為,本體定義了關鍵功能,而裝飾器提供或拒絕它的訪問,或者在訪問本體前做一些額外的事。

外觀模式

外觀模式的定義:即在內部讓多個方法一起被調用

涉及到兼容性,參數支持多格式,有很多這種代碼,對外暴露統一API,比如上面的$on支持數組,$off參數支持多種情況,對面只用一個函數,內部判斷實現

舉個簡單的栗子:

<code>// 封裝一些事件,讓其兼容各個瀏覽器
const myEvent = {
stopBubble(e){
if(typeof e.preventDefault() === 'function'){
e.preventDefault()
}
if(typeof e.stopPropagation() === 'function'){
e.stopPropagation()
}
// for IE
if(typeof e.returnValue === 'boolean'){
e.returnValue = false
}
if(typeof e.cancelBubble === 'boolean'){
e.cancelBubble = false
}
},
addEvent(dom, type, cb){
if(dom.addEventListener){
dom.addEventListener(type, cb, false)
} else if(dom.attachEvent){
dom.attachEvent('on' + type, cb)
}else{
dom['on' + type] = cb
}
}
}
複製代碼/<code>

以上就用外觀模式封裝了兩個基本事件,讓其兼容各種瀏覽器,調用者不需要知道內部的構造,只要知道這個方法怎麼用就行了。

工廠模式

工廠模式的定義:提供創建對象的接口,把成員對象的創建工作轉交給一個外部對象,好處就是消除對象直接的耦合(也就是相互影響)

常見的栗子,我們的彈窗message,對外部提供API,都是調用API,然後新建一個彈窗或者message的實例,就是典型的工程模式

簡單的栗子:

<code>class Man {
constructor(name) {
this.name = name
}
say(){
console.log(`我的名字 ` + this.name)
}
}
const p = new Man('JavaScript')
p.say() // 我的名字 JavaScript
複製代碼/<code>

當然工廠模式並不僅僅是用來 new 出實例

可以想象一個場景。假設有一份很複雜的代碼需要用戶去調用,但是用戶並不關心這些複雜的代碼,只需要你提供給我一個接口去調用,用戶只負責傳遞需要的參數,至於這些參數怎麼使用,內部有什麼邏輯是不關心的,只需要你最後返回我一個實例。這個構造過程就是工廠。

再比如下面Vue這個例子:

<code>const Notification = function(options) {
if (Vue.prototype.$isServer) return;
options = options || {};
let userOnClose = options.onClose;
let id = "notification_" + seed++;
let postion = options.postion || "top-right";
options.onClose = function() {
Notification.close(id, userOnClose);
};

instance = new NotificationConstructor({
data: options
});
if(isVNode(options.message)){
instance.$slots.default = [options.message]
options.message = 'REPLACED_BY_VNODE'
}
instance.id = id
instance.$mount()
document.body.appendChild(instance.$el)
instance.visible = true
instance.dom = instance.$el
instance.dom.style.zIndex = PopupManager.nextZIndex()
let verticalOffset = options.offset || 0
instances.filter(item => {
verticalOffset += item.$el.offsetHeight + 16
})
verticalOffset += 16
instance.verticalOffset = verticalOffset
instances.push(instance)
return instance

};
複製代碼/<code>

在上述代碼中,我們可以調用它封裝好的方法就可以創建對象實例,至於它內部的實現原理我們並不關心。

建造者模式Builder

建造者模式的定義:和工廠者模式相比,參與了更多創建過程或者更加複雜

<code>const Person = function(name, work){
// 創建應聘者緩存對象
let _person = new Human()

// 創建應聘者姓名解析對象
_person.name = new NamedNodeMap(name)

// 創建應聘者期望職位
_person.work = new Worker(work)

return _person
}
const p = new Person('小明', 'Java')
console.log(p)
複製代碼/<code>

迭代器模式

迭代器模式定義:指提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。迭代器模式可以把迭代的過程從業務邏輯中分離出來,在使用迭代器模式之後,即使不關心對象的內部構造,也可以按順序訪問其中的每個元素

比如常用的:every、map、filter、forEach等等

<code>const each = function(arr, callback){
if(!Array.isArray(arr)){
throw Error(`${arr} is not a Array`)
}
for(let i = 0, l = arr.length; i < l; i++){
callback.call(arr[i], i, arr[i])
}
}
each([1,2,4], function(i, n){
console.log([i, n])
})

複製代碼/<code>

享元模式

享元(flyweight)模式的定義:一種用於性能優化的模式,fly在這裡是蒼蠅的意思,意為蠅量級。享元模式的核心是運用共享技術來有效支持大量細粒度的對象。如果系統中因為創建了大量類似的對象而導致內存佔用過高,享元模式就是非常有用了。在JavaScript中,瀏覽器特別是移動端的瀏覽器分配的內存並不多,如何節省內存就成了一件非常有意義的事情

假設有個內衣工廠,目前的產品有50中男衣和50中女士內衣,為了推銷產品,工廠決定生產一些塑料模特來穿上他們的內衣拍成廣告照片。正常情況下需要50個男模特和50個女模特,然後讓他們每人分別穿上一件內衣來拍照

普通的做法:

<code>const Model = function(sex, underwear){
this.sex = sex
this.underwear = underwear
}
Model.prototype.takePhoto = function(){
console.log('sex=' + this.sex + ' underwear=' + this.underwear)
}
for(let i = 1; i <= 50; i++){
let maleModel = new Model('male', 'underwear' + i)
maleModel.takePhoto()
}
for(let join = 1; join <= 50; join++){
let femaleModel = new Model('female', 'underwear' + join)
femaleModel.takePhoto()
}

複製代碼/<code>

採用享元模式:

<code>const Model = function(sex){
this.sex = sex
}
Model.prototype.takePhoto = function(){
console.log('sex=' + this.sex + ' underwear=' + this.underwear)
}
// 分別創建一個男模特和一個女模特對象
let maleModel = new Model('male'),
femaleModel = new Model('female')
// 給男模特依次穿上所有的男裝,並進行拍照
for(let i = 1; i <= 50; i++){
maleModel.underwear = 'underwear' + i
maleModel.takePhoto()
}
// 給女模特依次穿上所有的女裝,並進行拍照
for(let j = 1; j <= 50; j++){
femaleModel.underwear = 'underwear' + j
femaleModel.takePhoto()
}
複製代碼/<code>
  • 內部狀態存儲於對象內部
  • 內部狀態可以被一些對象共享
  • 內部狀態獨立於具體的場景,通常不會改變
  • 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享

職責鏈模式

職責鏈模式的定義:使多個對象都有機會處理請求,從而避免請求發送者和接受者之間的耦合關係,將這些對象連成一條鏈,並沿著這條鏈傳遞該請求,知道有一個對象處理它為止。職責鏈模式的名字非常形象,一系列可能會處理請求的對象被連成一條鏈,請求在這些對象之間依次傳遞,知道遇到一個可以處理它的對象,我們把這些對象稱為鏈中的節點

簡單的栗子:假設我們負責一個售賣手機的電商網站,分別經過繳納500元定金和200元定金的兩輪預定後(訂單已在此時生成),現在已經到了正式購買的階段。公司針對支付過預定金的用戶有一定的優惠政策。在正式購買後,已經支付過500元定金的用戶會受到100元的商城優惠券,200元定金的用戶可以收到50元的優惠券,而之前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在存庫有限的情況下不一定保證買到

<code>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("庫存不足");
}
};
let Chain = function(fn) {
this.fn = fn;
this.successor = null;
};
// Chain.prototype.setNextSuccessor 指定在鏈中的下一個節點
Chain.prototype.setNextSuccessor = function(successor) {
return (this.successor = successor);
};
// Chain.prototype.passRequest 傳遞請求給某個節點
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;
};
let chainOrder500 = new Chain(order500)
let chainOrder200 = new Chain(order200)
let chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
// 500元定金預購,得到100元優惠券
chainOrder500.passRequest(1, true, 500)
// 200元定金預購,得到50元優惠券
chainOrder500.passRequest(2, true, 500)
// 普通購買,無優惠券

chainOrder500.passRequest(3, true, 500)
// 庫存不足
chainOrder500.passRequest(1, false, 0)
複製代碼/<code>

適配器模式

適配器模式定義:解決兩個軟件實體間的接口不兼容的問題。使用適配器模式之後,原本由於接口不兼容而不能工作的兩個軟件實體可以一起工作。適配器的別名是包裝器(wrapper),這是一個相對簡單的模式。在程序開發過程中有許多這樣的場景:當我們試圖調用模塊或者對象的某個接口時,卻發現這個接口的格式並不符合目前需求。這時候有兩種解決辦法,第一種是修改原來的接口實現,但如果原來的模板很複雜,或者我們拿到模塊是一段別人編寫的經過壓縮的代碼,修改原接口就顯得不太現實了。第二種方法是創建一個適配器,將原接口轉換為客戶希望的另一個接口,客戶只需要和適配器打交道

<code>let googleMap = {
show: function(){
console.log('開始渲染谷歌地圖')
}
}
let baiduMap = {
display: function(){
console.log('開始渲染百度地圖')
}
}
let baiduMapAdapter = {
show: function(){

return baiduMap.display()
}
}
renderMap(googleMap) // 開始渲染谷歌地圖
renderMap(baiduMapAdapter) // 開始渲染百度地圖
複製代碼/<code>

適配器模式主要用來解決兩個已有接口不匹配的問題,它不考慮這接口時怎麼實現的,也不考慮他們將來可能會如何演化。適配器模式不需要改變已有的接口,就能夠使他們協同作用

裝飾模式和代理模式也不會改變原有對象的接口,但裝飾器模式的作用是為了給對象增加功能。裝飾器模式常常形成一條長的裝飾鏈,適配器模式通常只包裝一次。代理模式為了控制對對象的訪問,通常也只包裝一次。

我們設計很多插件,有默認值,也算是適配器的一種應用,vue的prop校驗,default也算是適配器的應用了

外觀模式的作用倒是和適配器比較相似,有人把外觀模式看成一組對象的適配器,但外觀模式最顯著的特點是定義了一個新的接口。

模板方法模式

模板方法模式定義:在一個方法中定義一個算法骨架,而將一些步驟的實現延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中某些步驟的具體實現

我們常用的有很多,vue中的slot,react中的children

<code>class Parent {
constructor() {}
render() {


{/* 算法過程:children要渲染在name為joe的div中 */}
{this.props.children}


}
}
class Stage{
constructor(){}
render(){
// 在parent中已經設定了children的渲染位置算法
<parent>
// children的具體實現
child

/<parent>
}
}
複製代碼/<code>
<code><template>



<slot>


/<template>
<template>


<parent>

child

/<parent>

/<template>
複製代碼/<code>

中介者模式

中介者模式的定義:通過一箇中介者對象,其他所有的相關對象都通過該中介者來通信,而不是相互引用,當其中的一個對象發生改變時,只需要通知中介者對象即可。通過中介者模式可以解除對象與對象之間的緊耦合關係(目的就是減少耦合)

栗子:現實生活中,航線上的飛機只需要和機場的塔臺通信就能確定航線和飛行狀態,而不需要和所有飛機通信。同時塔臺作為中介者,知道每架飛機的飛行狀態,所以可以安排所有飛機的起降和航信安排。

中介者模式使用場景:例如購物車需求,存在商品選擇表單、顏色選擇表單、購買數量表單等等,都會觸發change事件,那麼可以通過中介者來轉發處理這些事件,實現各個事件間的解耦,僅僅維護中介者對象即可。

redux、vuex都屬於中介者模式的實際應用,我們把共享的數據,抽離成一個單獨的store,每個都通過tore這個中介者來操作對象

備忘錄模式

備忘錄模式定義:可以恢復到對象之前的某個狀態,其實大家學習react或者redux的時候,時間旅行的功能,就算是備忘錄模式的一個應用

<code>​```
複製代碼/<code>

推薦設計模式書籍

  • Head First設計模式
  • 大話設計模式
  • 設計模式(可複用面向對象軟件的基礎)(初學者不適)
  • JavaScript設計模式和開發實踐
  • 圖解設計模式

總結

創建設計模式:工廠,單例、建造者、原型

結構化設計模式:外觀,適配器,代理,裝飾器,享元,橋接,組合

行為型模式:策略、模板方法、觀察者、迭代器、責任鏈、命令、備忘錄、狀態、訪問者、終結者、解釋器

以上如有錯誤,歡迎糾正


原鏈接:https://juejin.im/post/5e4a87776fb9a07ca714ae54


分享到:


相關文章: