JavaScript 實現四種重要的設計模式

什麼是設計模式?前端

設計模式就是代碼問題的一套解決方案,或者說,解決特定問題的「最佳實踐」。node

「每個模式描述了一個在咱們周圍不斷重複發生的問題,以及該問題的解決方案的核心。這樣。你就能一次又一次地使用該方案而沒必要作重複勞動」 - Christopher Alexander程序員

常見的設計模式大概有23種,這裏以一張圖片說明。數據庫

image.png

本文先聊聊如下四種設計模式。編程

  • 對象 ✅
  • 組合模式(Composite Pattern) ✅
  • 適配器模式( Adapter Pattern) ✅
  • 裝飾者模式( Decorator Pattern) ✅

對象

JS 面向對象的改造

特別說明:實際開發過程當中儘可能不要修改對象的.prototype屬性,下面的寫法僅僅是爲了展現。json

// 原始代碼
function startAnimation() {
    console.log('start animation')
}
function stopAnimation() {
    console.log('stop animation')
}

// 改成面向對象的方式
var Anim = function() {
}
Anim.prototype.start = function() {
    console.log('start animation')
}
Anim.prototype.stop = function() {
    console.log('stop animation')
}
var myAnim = new Anim()
myAnim.start()
myAnim.stop()

// 變形簡化
var Anim = function() {}
Anim.prototype = {
    start: function() {
        console.log('start')
    },
    stop: function() {
        console.llog('end')
    }
}

// 持續改進
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
}
var Anim = function() {
}
Anim.method('start', function() {
    console.log('start animation')
})
Anim.method('stop', function() {
    console.log('stop animation')
})

// 繼續改進,鏈式調用
Function.prototype.method = function(name, fn) {
    this.prototype[name] = fn
    return this
}
var Anim = function() {}
Anim.method('start', function() {
    console.log('start animation')
}).method('stop', function() {
    console.log(stop animation') }) 複製代碼

JS 匿名函數

// 如下是當即執行的函數表達式
(function(){
    var foo = 1
    var bar = 2
    console.log(foo * bar)
})()

// 攜帶參數
(function(foo, bar){
   console.log(foo * bar)
})(1, 2)

// 閉包,訪問函數內部的局部變量
var baz
(function(){
    var foo = 1
    var bar = 2
    baz = function() {
        return foo * bar
    }
})()
baz()
複製代碼

組合模式(Composite Pattern)

image.png

特色:設計模式

  • 層層嵌套,父節點 - 葉子節點
  • 父節點和葉子節點有相同的接口以及不一樣的實現
class Container {
  constructor(id) {
    this.children = []
    this.element = document.createElement('div')
    this.element.id = id
    this.element.style.border = '1px solid black'
    this.element.style.margin = '10px'
    this.element.classList.add('container')    
  }

  add(child) {
    this.children.push(child)
    this.element.appendChild(child.getElement())
  }


  hide() {
    this.children.forEach(node => node.hide())
    this.element.style.display = 'none'
  }

  show() {
    this.children.forEach(node => node.show())
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }

}

class Text {
  constructor(text) {
    this.element = document.createElement('p')
    this.element.innerText = text
  }

  add() {}

  hide() {
    this.element.style.display = 'none'
  }

  show() {
    this.element.style.display = ''
  }

  getElement() {
    return this.element
  }
}

let header = new Container('header')
header.add(new Text('標題'))
header.add(new Text('logo'))

let main = new Container('main')
main.add(new Text('這是內容1'))
main.add(new Text('這是內容2'))

let page = new Container('page')
page.add(header)
page.add(main)
page.show()

document.body.appendChild(page.getElement())
複製代碼

適配器模式(Adapter Pattern)

In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code. --from wiki服務器

一句話,適配器模式能夠類比於家裏的插座,上面有不一樣類型的插孔,用來適配不一樣的插頭。babel

實際的開發場景

  • 場景1markdown

    使用nodejs作一個ORM框架,給用戶暴露一套統一的數據庫操做接口,底層根據數據庫類型適配不一樣數據庫。

  • 場景2

    作一個日誌模塊,給用戶暴露一套統一的記錄日誌接口,底層根據類型適配是文件存儲日誌仍是數據庫存儲日誌。

  • 場景3

    前端開發過程當中需用到獲取數據和保存數據。在開發階段,能夠把數據存儲和查詢用 localStorage 來作;接口就緒後能夠發送請求從服務器存取數據。使用適配器模式,爲使用者提供統一接口。

如下爲適配器模式的代碼示例。

const  localStorageAdapter = {
  findAll: function(callback) {
    let cartList = JSON.parse(localStorage['cart'])
    callback(cartList)
  },
  save: function(item) {
    let cartList = JSON.parse(localStorage['cart'])
    cartList.push(item)
    localStorage['cart'] = JSON.stringify(cartList)
  }
}

const  serverAdapter = {
  findAll: function(callback) {
      fetch('https://someAPI.com/getCartList')
        .then(res => res.json())
        .then(data => callback(data))
  },
  save: function(item) {
    fetch('https://someAPI.com/addToCart', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item) })
      .then(res => res.json())
      .then(data => callback(data))
  }
}

class ShoppingCart {
    constructor(adapter) {
        this.adapter = adapter
    }
    add(item) {
        this.adapter.save(item)
    }
    show() {
        this.adapter.findAll(list => {
            console.log(list)
        } )
    }
}

let cart = new ShoppingCart(localStorageAdapter) // 使用了 localStorage 這個插孔
// let cart = new ShoppingCart(serverAdapter)
cart.add({title: '手機'})
cart.add({title: '電腦'})
cart.show()
複製代碼

裝飾者模式(Decorator Pattern)

先問一個問題:如何給一個對象增長額外的功能?

方法1:直接修改對象

方法2:建立子類繼承自父類,子類實例化新對象(若是太細,會致使子類對象的泛濫)

方法3:不改變原對象,在原對象基礎上進行「裝飾」,新增一些和核心功能無關的功能

裝飾器模式即爲上述方法3

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. --wiki

套用 Wiki 的描述,裝飾器模式的最大特色是保留主幹,增長裝飾,但不影響原功能。假如用一個成語來形容,我認爲最好的莫過於「錦上添花」了。

使用場景

  • 場景1:Form表單組件,用戶點擊提交時把用戶內容提交。增長裝飾:提交以前作個校驗

  • 場景2:一個功能正常執行。增長裝飾:在執行前記錄下起始時間,在執行後記錄下結束時間並計算消耗時間

  • 場景3: 用戶點擊按鈕,執行某個功能。增長裝飾:在執行前發請求到統計平臺,統計用戶的點擊次數

  • 場景4:給一個編輯器組件增長一個輸入改變時,保存數據到服務器節流的裝飾

AOP 面向切面編程

Java 大名鼎鼎的 Spring 框架的核心編程思想 - AOP 面向切面編程,裝飾器模式就是它比較常見的實現。

下面用 JS 來實現一個簡單的 AOP 對象。

const AOP = {}
AOP.before = function (fn, before) {
    return function() {
        before.apply(this,arguments)
        fn.apply(this, arguments)
    }
}
AOP.after = function(fn, after) {
    return function () {
        fn.apply(this, arguments)
        after.apply(this, arguments)
    }
}

// 點擊按鈕提交數據
function submit() {
    console.log('提交數據')
}

document.querySelector('.btn').onclick = submit

// 在原有功能基礎上作點裝飾:點擊按鈕,提交數據前作個校驗
function submit() {
    console.log(this)
    console.log('提交數據')
}
function check() {
    console.log(this)
    console.log('先進行校驗')
}
submit = AOP.before(submit, check)
document.querySelector('.btn').onclick = submit
複製代碼

ES7 Decorator 修飾符

借鑑其餘語言的優點,JS 在 ES7 推出了 Decorator 修飾符,很差理解也不太實用,但若是熟悉裝飾器模式的話,理解起來就容易多了:這不就是裝飾器模式的語法糖嘛,示例以下。

const logWrapper = targetClass => {
    let orignRender = targetClass.prototype.render
    targetClass.prototype.render = function(){
        console.log("before render")
        orignRender.apply(this) 
        console.log("after render")
    }
    return targetClass
}


class App {
    constructor() {
      this.title = '首頁'
    }
    render(){
        console.log('渲染頁面:' + this.title);
    }
}

App = logWrapper(App)

new App().render()

// 使用 decorator 修飾符,修改class
const logWrapper = targetClass => {
  let orignRender = targetClass.prototype.render
  targetClass.prototype.render = function(){
    console.log("before render")
    orignRender.apply(this) 
    console.log("after render")
  }
  return targetClass
}

@logWrapper
class App {
  constructor() {
    this.title = '首頁'
  }
  render(){
    console.log('渲染頁面:' + this.title);
  }
}
複製代碼

使用 Decorator 修飾符,修改原型屬性

Tips: 運行代碼需babel支持,把JavaScript模式調整到ES6/babel

function logWrapper(target, name, descriptor) {
    console.log(arguments)
    let originRender = descriptor.value
    descriptor.value = function() {
      console.log('before render')
      originRender.bind(this)()
      console.log('after render')
    }
    console.log(target)
}

class App {
  constructor() {
    this.title = '首頁'
  }
  @logWrapper
  render(){
    console.log('渲染頁面:' + this.title);
  }
}

new App().render()
複製代碼

總結

設計模式看起來彷佛有些枯燥乏味,學習起來短時間可能也並不能見到效果,但當業務變得複雜、代碼變得愈來愈冗餘時,設計模式就能給你正確的指引。畢竟代碼相關的「最佳實踐」流傳至今,一定有它存在的道理。

本文內容大多來自經典書籍 《Pro JavaScript Design Patterns》,這本書講解了十幾種 JavaScript 經典的設計模式。學習設計模式不只能大幅提升 JavaScript 的內功修爲,更是程序員進階之路上必不可少的一座山。

相關文章
相關標籤/搜索