JavaScript 的一些經常使用設計模式

學習和總結文章同步發佈於 https://github.com/xianshanna...,有興趣能夠關注一下,一塊兒學習和進步。
設計模式的定義:在面向對象軟件設計過程當中針對特定問題的簡潔而優雅的解決方案

設計模式是前人解決某個特定場景下對而總結出來的一些解決方案。可能剛開始接觸編程尚未什麼經驗的時候,會感受設計模式沒那麼好理解,這個也很正常。有些簡單的設計模式咱們有時候用到,不過沒意識到也是存在的。html

學習設計模式,可讓咱們在處理問題的時候提供更多更快的解決思路。前端

固然設計模式的應用也不是一時半會就會上手,不少狀況下咱們編寫的業務邏輯都沒用到設計模式或者原本就不須要特定的設計模式。node

適配器模式

這個使咱們常使用的設計模式,也算最簡單的設計模式之一,好處在於能夠保持原有接口的數據結構不變更。git

適配器模式(Adapter Pattern)是做爲兩個不兼容的接口之間的橋樑。

例子

適配器模式很好理解,假設咱們和後端定義了一個接口數據結構爲(能夠理解爲舊接口):github

[
  {
    "label": "選擇一",
    "value": 0
  },
  {
    "label": "選擇二",
    "value": 1
  }
]

可是後端後面由於其餘緣由,須要定義返回的結構爲(能夠理解爲新接口):算法

[
  {
    "label": "選擇一",
    "text": 0
  },
  {
    "label": "選擇二",
    "text": 1
  }
]

而後咱們前端的使用到後端接口有好幾處,那麼我能夠把新的接口字段結構適配爲老接口的,就不須要各處去修改字段,只要把源頭的數據適配好就能夠了。編程

固然上面的是很是簡單的場景,也是常常用到的場景。或許你會認爲後端處理不更好了,的確是這樣更好,可是這個不是咱們討論的範圍。json

單例模式

單例模式,從字面意思也很好理解,就是實例化屢次都只會有一個實例。後端

有些場景實例化一次,能夠達到緩存效果,能夠減小內存佔用。還有些場景就是必須只能實例化一次,不然實例化屢次會覆蓋以前的實例,致使出現 bug(這種場景比較少見)。設計模式

例子

實現彈框的一種作法是先建立好彈框, 而後使之隱藏, 這樣子的話會浪費部分沒必要要的 DOM 開銷, 咱們能夠在須要彈框的時候再進行建立, 同時結合單例模式實現只有一個實例, 從而節省部分 DOM 開銷。下列爲登入框部分代碼:

const createLoginLayer = function() {
  const div = document.createElement('div')
  div.innerHTML = '登入浮框'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}

使單例模式和建立彈框代碼解耦

const getSingle = function(fn) {
  const result
  return function() {
    return result || result = fn.apply(this, arguments)
  }
}
const createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function() {
  createSingleLoginLayer()
}

代理模式

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

代理對象擁有本體對象的一切功能的同時,能夠擁有而外的功能。並且代理對象和本體對象具備一致的接口,對使用者友好。

虛擬代理

下面這段代碼運用代理模式來實現圖片預加載,能夠看到經過代理模式巧妙地將建立圖片與預加載邏輯分離,,而且在將來若是不須要預加載,只要改爲請求本體代替請求代理對象就行。

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()

const proxyImage = (function() {
  const img = new Image()
  img.onload = function() { // http 圖片加載完畢後纔會執行
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('loading.jpg') // 本地 loading 圖片
      img.src = src
    }
  }
})()

proxyImage.setSrc('http://loaded.jpg')

緩存代理

在原有的功能上加上結果緩存功能,就屬於緩存代理。

原先有個功能是實現字符串反轉(reverseString),那麼在不改變 reverseString 的現有邏輯,咱們可使用緩存代理模式實現性能的優化,固然也能夠在值改變的時候去處理下其餘邏輯,如 Vue computed 的用法。

function reverseString(str) {
  return str
    .split('')
    .reverse()
    .join('')
}
const reverseStringProxy = (function() {
  const cached = {}
  return function(str) {
    if (cached[str]) {
      return cached[str]
    }
    cached[str] = reverseString(str)
    return cached[str]
  }
})()

訂閱發佈模式

軟件架構中, 發佈-訂閱是一種 消息 範式,消息的發送者(稱爲發佈者)不會將消息直接發送給特定的接收者(稱爲訂閱者)。而是將發佈的消息分爲不一樣的類別,無需瞭解哪些訂閱者(若是有的話)可能存在。一樣的,訂閱者能夠表達對一個或多個類別的興趣,只接收感興趣的消息,無需瞭解哪些發佈者(若是有的話)存在。

或許你用過 eventemitter、node 的 events、Backbone 的 events 等等,這些都是前端早期,比較流行的數據流通訊方式,即訂閱發佈模式

從字面意思來看,咱們須要首先訂閱,發佈者發佈消息後纔會收到發佈的消息。不過咱們還須要一箇中間者來協調,從事件角度來講,這個中間者就是事件中心,協調發布者和訂閱者直接的消息通訊。

完成訂閱發佈整個流程須要三個角色:

  • 發佈者
  • 事件中心
  • 訂閱者

    訂閱者是能夠多個的。

以事件爲例,簡單流程以下:

發佈者->事件中心<=>訂閱者,訂閱者須要向事件中心訂閱指定的事件 -> 發佈者向事件中心發佈指定事件內容 -> 事件中心通知訂閱者 -> 訂閱者收到消息(多是多個訂閱者),到此完成了一次訂閱發佈的流程。

簡單的代碼實現以下:

class Event {
  constructor() {
    // 全部 eventType 監聽器回調函數(數組)
    this.listeners = {}
  }
  /**
   * 訂閱事件
   * @param {String} eventType 事件類型
   * @param {Function} listener 訂閱後發佈動做觸發的回調函數,參數爲發佈的數據
   */
  on(eventType, listener) {
    if (!this.listeners[eventType]) {
      this.listeners[eventType] = []
    }
    this.listeners[eventType].push(listener)
  }
  /**
   * 發佈事件
   * @param {String} eventType 事件類型
   * @param {Any} data 發佈的內容
   */
  emit(eventType, data) {
    const callbacks = this.listeners[eventType]
    if (callbacks) {
      callbacks.forEach((c) => {
        c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {
  console.log(data)
})
event.emit('open', { open: true })

Event 能夠理解爲事件中心,提供了訂閱和發佈功能。

訂閱者在訂閱事件的時候,只關注事件自己,而不關心誰會發布這個事件;發佈者在發佈事件的時候,只關注事件自己,而不關心誰訂閱了這個事件。

觀察者模式

觀察者模式定義了一種一對多的依賴關係,讓多個 觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知全部 觀察者對象,使它們可以自動更新。

觀察者模式咱們可能比較熟悉的場景就是響應式數據,如 Vue 的響應式、Mbox 的響應式。

觀察者模式有完成整個流程須要兩個角色:

  • 目標
  • 觀察者

簡單流程以下:

目標<=>觀察者,觀察者觀察目標(監聽目標)-> 目標發生變化-> 目標主動通知觀察者(多是多個)。

簡單的代碼實現以下:

/**
 * 觀察監聽一個對象成員的變化
 * @param {Object} obj 觀察的對象
 * @param {String} targetVariable 觀察的對象成員
 * @param {Function} callback 目標變化觸發的回調
 */
function observer(obj, targetVariable, callback) {
  if (!obj.data) {
    obj.data = {}
  }
  Object.defineProperty(obj, targetVariable, {
    get() {
      return this.data[targetVariable]
    },
    set(val) {
      this.data[targetVariable] = val
      // 目標主動通知觀察者
      callback && callback(val)
    },
  })
  if (obj.data[targetVariable]) {
    callback && callback(obj.data[targetVariable])
  }
}

可運行例子以下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <title></title>
  </head>
  <body>
    <div id="app">
      <div id="dom-one"></div>
      <br />
      <div id="dom-two"></div>
      <br />
      <button id="btn">改變</button>
    </div>
    <script>
      /**
       * 觀察監聽一個對象成員的變化
       * @param {Object} obj 觀察的對象
       * @param {String} targetVariable 觀察的對象成員
       * @param {Function} callback 目標變化觸發的回調
       */
      function observer(obj, targetVariable, callback) {
        if (!obj.data) {
          obj.data = {}
        }
        Object.defineProperty(obj, targetVariable, {
          get() {
            return this.data[targetVariable]
          },
          set(val) {
            this.data[targetVariable] = val
            // 目標主動通知觀察者
            callback && callback(val)
          },
        })
        if (obj.data[targetVariable]) {
          callback && callback(obj.data[targetVariable])
        }
      }

      const obj = {
        data: { description: '原始值' },
      }

      observer(obj, 'description', value => {
        document.querySelector('#dom-one').innerHTML = value
        document.querySelector('#dom-two').innerHTML = value
      })

      btn.onclick = () => {
        obj.description = '改變了'
      }
    </script>
  </body>
</html>

裝飾者模式

裝飾器模式(Decorator Pattern)容許向一個現有的對象添加新的功能,同時又不改變其結構。

ES6/7 的decorator 語法提案,就是裝飾者模式。

例子

class A {
  getContent() {
    return '第一行內容'
  }
  render() {
    document.body.innerHTML = this.getContent()
  }
}

function decoratorOne(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      第一行以前的內容
      <br/>
      ${prevGetContent()}
    `
  }
  return cla
}

function decoratorTwo(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      ${prevGetContent()}
      <br/>
      第二行內容
    `
  }
  return cla
}

const B = decoratorOne(A)
const C = decoratorTwo(B)
new C().render()

策略模式

在策略模式(Strategy Pattern)中,一個行爲或其算法能夠在運行時更改。

假設咱們的績效分爲 A、B、C、D 這四個等級,四個等級的獎勵是不同的,通常咱們的代碼是這樣實現:

/**
 * 獲取年終獎
 * @param {String} performanceType 績效類型,
 * @return {Object} 年終獎,包括獎金和獎品
 */
function getYearEndBonus(performanceType) {
  const yearEndBonus = {
    // 獎金
    bonus: '',
    // 獎品
    prize: '',
  }
  switch (performanceType) {
    case 'A': {
      yearEndBonus = {
        bonus: 50000,
        prize: 'mac pro',
      }
      break
    }
    case 'B': {
      yearEndBonus = {
        bonus: 40000,
        prize: 'mac air',
      }
      break
    }
    case 'C': {
      yearEndBonus = {
        bonus: 20000,
        prize: 'iphone xr',
      }
      break
    }
    case 'D': {
      yearEndBonus = {
        bonus: 5000,
        prize: 'ipad mini',
      }
      break
    }
  }
  return yearEndBonus
}

使用策略模式能夠這樣:

/**
 * 獲取年終獎
 * @param {String} strategyFn 績效策略函數
 * @return {Object} 年終獎,包括獎金和獎品
 */
function getYearEndBonus(strategyFn) {
  if (!strategyFn) {
    return {}
  }
  return strategyFn()
}

const bonusStrategy = {
  A() {
    return {
      bonus: 50000,
      prize: 'mac pro',
    }
  },
  B() {
    return {
      bonus: 40000,
      prize: 'mac air',
    }
  },
  C() {
    return {
      bonus: 20000,
      prize: 'iphone xr',
    }
  },
  D() {
    return {
      bonus: 10000,
      prize: 'ipad mini',
    }
  },
}

const performanceLevel = 'A'
getYearEndBonus(bonusStrategy[performanceLevel])

這裏每一個函數就是一個策略,修改一個其中一個策略,並不會影響其餘的策略,均可以單獨使用。固然這只是個簡單的範例,只爲了說明。

策略模式比較明顯的特性就是能夠減小 if 語句或者 switch 語句。

職責鏈模式

顧名思義,責任鏈模式(Chain of Responsibility Pattern)爲請求建立了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬於行爲型模式。

在這種模式中,一般每一個接收者都包含對另外一個接收者的引用。若是一個對象不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。

例子

function order(options) {
  return {
    next: (callback) => callback(options),
  }
}

function order500(options) {
  const { orderType, pay } = options
  if (orderType === 1 && pay === true) {
    console.log('500 元定金預購, 獲得 100 元優惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function order200(options) {
  const { orderType, pay } = options
  if (orderType === 2 && pay === true) {
    console.log('200 元定金預購, 獲得 50 元優惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function orderCommon(options) {
  const { orderType, stock } = options
  if (orderType === 3 && stock > 0) {
    console.log('普通購買, 無優惠券')
    return {}
  } else {
    console.log('庫存不夠, 沒法購買')
  }
}

order({
  orderType: 3,
  pay: true,
  stock: 500,
})
  .next(order500)
  .next(order200)
  .next(orderCommon)
// 打印出 「普通購買, 無優惠券」

上面的代碼,對 order 相關的進行了解耦,order500order200orderCommon 等都是能夠單獨調用的。

參考文章

相關文章
相關標籤/搜索