JavaScript 的一些常用設計模式
設計模式是前人解決某個特定場景下對而總結出來的一些解決方案。可能剛開始接觸編程還沒有什么經驗的時候,會感覺設計模式沒那么好理解,這個也很正常。有些簡單的設計模式我們有時候用到,不過沒意識到也是存在的。
學習設計模式,可以讓我們在處理問題的時候提供更多更快的解決思路。
當然設計模式的應用也不是一時半會就會上手,很多情況下我們編寫的業務邏輯都沒用到設計模式或者本來就不需要特定的設計模式。
適配器模式
這個使我們常使用的設計模式,也算最簡單的設計模式之一,好處在于可以保持原有接口的數據結構不變動。
適配器模式(Adapter Pattern)是作為兩個不兼容的接口之間的橋梁。
例子
適配器模式很好理解,假設我們和后端定義了一個接口數據結構為(可以理解為舊接口):
- [
- {
- "label": "選擇一",
- "value": 0
- },
- {
- "label": "選擇二",
- "value": 1
- }
- ]
但是后端后面因為其他原因,需要定義返回的結構為(可以理解為新接口):
- [
- {
- "label": "選擇一",
- "text": 0
- },
- {
- "label": "選擇二",
- "text": 1
- }
- ]
然后我們前端的使用到后端接口有好幾處,那么我可以把新的接口字段結構適配為老接口的,就不需要各處去修改字段,只要把源頭的數據適配好就可以了。
當然上面的是非常簡單的場景,也是經常用到的場景。或許你會認為后端處理不更好了,的確是這樣更好,但是這個不是我們討論的范圍。
單例模式
單例模式,從字面意思也很好理解,就是實例化多次都只會有一個實例。
有些場景實例化一次,可以達到緩存效果,可以減少內存占用。還有些場景就是必須只能實例化一次,否則實例化多次會覆蓋之前的實例,導致出現 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]
- }
- })()
訂閱發布模式
訂閱發布使前端常用的數據通信方式、異步邏輯處理等等,如 React setState 和 Redux 就是訂閱發布模式的。
但是要合理的使用訂閱發布模式,否則會造成數據混亂,redux 的單向數據流思想可以避免數據流混亂的問題。
例子
- 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 })
觀察者模式
觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新。
Vue 的數據驅動就是使用觀察者模式,mbox 也是使用觀察者模式。
例子
模仿 Vue 數據驅動渲染模式(只是類似,簡單的模仿)。
首先使用 setter 和 getter 監聽到數據的變化:
- const obj = {
- data: { description: '' },
- }
- Object.defineProperty(obj, 'description', {
- get() {
- return this.data.description
- },
- set(val) {
- this.data.description = val
- },
- })
然后加上目標和觀察者
- class Subject {
- constructor() {
- this.observers = []
- }
- add(observer) {
- this.observers.push(observer)
- }
- notify(data) {
- this.observers.forEach((observer) => observer.update(data))
- }
- }
- class Observer {
- constructor(callback) {
- this.callback = callback
- }
- update(data) {
- this.callback && this.callback(data)
- }
- }
- // 創建觀察者ob1
- let ob1 = new Observer((text) => {
- document.querySelector('#dom-one').innerHTML(text)
- })
- // 創建觀察者ob2
- let ob2 = new Observer((text) => {
- document.querySelector('#dom-two').innerHTML(text)
- })
- // 創建目標sub
- let sub = new Subject()
- // 目標sub添加觀察者ob1 (目標和觀察者建立了依賴關系)
- sub.add(ob1)
- // 目標sub添加觀察者ob2
- sub.add(ob2)
- // 目標sub觸發事件(目標主動通知觀察者)
- sub.notify('這里改變了')
組合在一起是這樣的
- <!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>
- class Subject {
- constructor() {
- this.observers = []
- }
- add(observer) {
- this.observers.push(observer)
- }
- notify() {
- this.observers.forEach((observer) => observer.update())
- }
- }
- class Observer {
- constructor(callback) {
- this.callback = callback
- }
- update() {
- this.callback && this.callback()
- }
- }
- const obj = {
- data: { description: '' },
- }
- // 創建觀察者ob1
- const ob1 = new Observer(() => {
- console.log(document.querySelector('#dom-one'))
- document.querySelector('#dom-one').innerHTML = obj.description
- })
- // 創建觀察者ob2
- const ob2 = new Observer(() => {
- document.querySelector('#dom-two').innerHTML = obj.description
- })
- // 創建目標sub
- const sub = new Subject()
- // 目標sub添加觀察者ob1 (目標和觀察者建立了依賴關系)
- sub.add(ob1)
- // 目標sub添加觀察者ob2
- sub.add(ob2)
- Object.defineProperty(obj, 'description', {
- get() {
- return this.data.description
- },
- set(val) {
- this.data.description = val
- // 目標sub觸發事件(目標主動通知觀察者)
- sub.notify()
- },
- })
- 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 相關的進行了解耦,order500,order200、orderCommon 等都是可以單獨調用的。