JavaScript設計模式之裝飾者模式

定義

裝飾者模式能夠動態地給某個對象添加一些額外的職責,而不會影響這個類中派生的其餘對象。裝飾模式可以在不改變對象自身的基礎上,在程序運行期間給對象動態地添加職責,跟繼承相比,裝飾者更加輕便靈活。javascript

使用面向對象實現裝飾者模式

假設咱們編寫一個飛機大戰的遊戲,飛機會根據經驗值的增長升級子彈的類型,一開始飛機只能發射普通子彈,升到二級能夠發射導彈,升到三級能夠發射原子彈。用代碼實現以下:java

var Plane = function () {}

Plane.prototype.fire = function () {
  console.log('發射子彈')
}

var MissleDecorator = function (plane) {
  this.plane = plane
}

MissleDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('發射導彈')
}

var AtomDecorator = function (plane) {
  this.plane = plane
}

AtomDecorator.prototype.fire = function () {
  this.plane.fire()
  console.log('發射原子彈')
}

// 應用
let plane = new Plane()
plane = new MissleDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() // 發送普通子彈、發送導彈、發送原子彈
複製代碼

導彈和原子彈裝飾類的構造函數都接受plane對象,而且保存這個參數,在它們的fire方法中,除了自身的操做,還要調用plane對象的fire方法。這種方式沒有改變plane對象的自身,而是將對象傳遞給另外一個對象,這些對象以一條鏈的方式進行引用,造成聚合對象。
能夠看到裝飾者對象和它所裝飾的對象擁有一致的接口,因此它們對使用該對象的客戶來講是透明的,被裝飾對象也不須要知道它曾經被裝飾過,這種透明性使得咱們能夠嵌套任意多個裝飾對象。web

JavaScript中的裝飾者

JavaScript語言的動態性使得改變對象很容易,咱們能夠直接改寫對象或者某個對象的方法,並不須要用「類」來裝飾,使用JavaScript實現上面例子的代碼以下:ajax

const plane = {
  fire () {
    console.log('發射子彈')
  }
}

const missleDecorator = function () {
  console.log('發射導彈')
}

const atomDecorator = function () {
  console.log('發射原子彈')
}

const copyFire1 = plane.fire
plane.fire = function () {
  copyFire1()
  missleDecorator()
}

const copyFir2 = plane.fire
plane.fire = function () {
  copyFire2()
  atomDecorator()
}

plane.fire() // 發送普通子彈、發送導彈、發送原子彈
複製代碼

裝飾函數

在JavaScript中,幾乎一切都是對象,函數又被稱爲一等對象。在JavaScript中能夠很方便地修改對象的屬性和方法,因此要爲函數添加功能,最簡單粗暴的方式是直接改寫函數,可是這違反開放-封閉原則。好比下面的例子:算法

var a = function () {
  console.log(1)
}
// 改爲
var a = function () {
  console.log(1)
  console.log(2)
}
複製代碼

可是若是某個函數很複雜,並且以前可能也不是你維護,隨便修改極可能產生難以預料的Bug,因而咱們從裝飾者模式中找到了一種答案,保存原函數的引用,而後添加新的功能:服務器

var a = function () {
  console.log(1)
}

var _a = a

a = function () {
  _a()
  console.log(2)
}
複製代碼

在實際開發中,這也是一種常見的作法。好比咱們想給window綁定onload事件,可是不肯定這個事件是否是被其餘人綁定過,因而爲了以前的函數不被覆蓋,有以下代碼:app

window.onload = function () {
  console.log(1)
}

var _onload = window.onload || function () {}
window.onload = function () {
  _onload()
  console.log(2)
}
複製代碼

這樣的代碼是符合開放-封閉原則的,咱們增長新的功能的時候,沒有修改原來的代碼。但這種方式有一些缺點:異步

  • 必須維護_onload這個中間變量,若是函數的裝飾鏈較長,或者裝飾的函數變多,這些中間變量的數量也會愈來愈多。
  • 還會有this被劫持的問題,在上面的例子中沒有問題,由於調用普通函數_onload,this也指向window,如今把window.onload改爲document.getElementById,代碼以下:
var _getElementById = document.getElementById

document.getElementById = function (id) {
  console.log(1)
  return _getElementById(id)
}

var button = document.getElementById('button')
複製代碼

執行這段代碼,控制檯在打印1後,拋出以下異常:函數

// Uncaught TypeError: Illegal invocation
複製代碼

異常的緣由就是此時_getElementById是一個全局函數,調用全局函數時,this指向window的,而document.getElementById內部this預期的指向是document。因此咱們須要改進代碼:post

var _getElementById = document.getElementById

document.getElementById = function (id) {
  console.log(1)
  return _getElementById.call(document, id)
}

var button = document.getElementById('button')
複製代碼

使用AOP裝飾函數

首先定義兩個函數Function.prototype.before和Function.prototype.after:

Function.prototype.before = function (beforeFn) {
  var self = this

  return function () {
    beforeFn.apply(this, arguments)

    return self.apply(this, arguments)
  }
}

Function.prototype.after = function (afterFn) {
  var self = this

  return function () {
    const ret = self.apply(this, arguments)
    afterFn.apply(this, arguments)

    return ret
  }
}
複製代碼

Function.prototype.before接受一個函數做爲參數,這個函數即爲要添加的裝飾函數,它裏面有須要添加的新功能的代碼。
接着把當前的this保存起來,這個this指向原函數,返回一個代理函數。這個代理函數的做用是把請求分別轉發給新添加的函數和原函數,而且保證它們的執行順序,讓新添加的函數在原函數以前執行,也叫前置裝飾,這樣就實現了動態裝飾的效果。
由於咱們在函數中保存了this,經過apply函數綁定正確的this,保證函數在被裝飾以後,this不會被劫持。因而前面的例子,咱們能夠這樣寫:

document.getElementById = document.getElementById.before(function () {
   console.log(1)
 })

 console.log(document.getElementById('button'))
複製代碼

AOP應用

數據統計上報

分離業務代碼和數據統計代碼,不管在什麼語言中,都是AOP的經典應用之一。在項目的開發結尾的時候,咱們通常須要加一些統計數據的代碼,這些過程可能讓咱們被迫改動已經封裝好的函數。好比,頁面中有登陸按鈕,點擊登陸按鈕,彈出登陸彈窗的同時還要上報數據,來統計有多少用戶點擊了登陸按鈕。下面簡單的代碼實現:

function showLoginModal () {
  console.log('打開登陸彈窗')
  log('傳入一些按鈕信息')
}

function log (info) {
  console.log('上報用戶信息和按鈕信息到服務器')
}

document.getElementById('loginBtn').onclick = showLoginModal
複製代碼

能夠看到,在showLogin函數裏,既要負責打開彈窗的功能,又要負責數據上報,這兩個不一樣層面的代碼耦合在一塊兒,咱們可使用AOP進行優化:

var showLoginModal = function () {
  console.log('打開登陸彈窗')
  log('傳入一些按鈕信息')
}

function log (info) {
  console.log('上報用戶信息和按鈕信息到服務器')
}

showLoginModal = showLoginModal.after(log)  // 打開彈窗以後上報數據

document.getElementById('loginBtn').onclick = showLoginModal
複製代碼

使用AOP動態改變函數的參數

觀察Function.prototype.before函數:

Function.prototype.before = function (beforeFn) {
  var self = this

  return function () {
    beforeFn.apply(this, arguments)

    return self.apply(this, arguments)
  }
}
複製代碼

能夠看到beforeFn函數和原函數共用參數arguments,因此咱們在beforeFn中修改參數後,原函數接收的參數也會發生變化。
如今有一個用於ajax請求的函數,它負責項目中全部的ajax異步請求:

var ajax = function (type, url, param) {
  console.dir(param)

  // 這裏是發送請求的代碼
}

ajax('get', 'http://xxx.com/userInfo', { name: 'uzi' })
複製代碼

上面代碼表示向服務端發起一個獲取用戶信息的請求,傳遞的參數是{ name: 'uzi' }。ajax函數在項目中一直工做良好,忽然有有一天,網站遭受了CSRF攻擊,解決CSRF的一個辦法就是在全部HTTP請求中帶上一個token參數。因而咱們定義了一個生成token的函數:

var getToken = function () {
  return 'token'
}
複製代碼

下面給全部請求加上token參數:

var ajax = function (type, url, param) {
  param = param || {}
  param.token = getToken()

  // 這裏是發送請求的代碼
}
複製代碼

這樣問題就解決了,可是ajax函卻變得僵硬了,雖然每一個ajax請求都自動帶上了token參數,在當前項目是沒有什麼問題。可是,若是未來要將這個ajax函數封裝到公司的通用庫裏,那這個token參數可能就是多餘的了,也許另外一個項目不須要token參數,或者生成token的算法不同,不管怎麼樣,都須要修改這個ajax函數。咱們用AOP來解決這個問題:

var ajax = function (type, url, param) {
  console.dir(param)

  // 這裏是發送請求的代碼
}

var getToken = function () {
  return 'token'
}

// 使用Function.prototype.before裝飾ajax函數
ajax = ajax.before(function (type, url, param) {
  param.token = getToken()
})

ajax('get', 'http://xxx.com/userInfo', { name: 'uzi' })  // { name: 'uzi', token: 'token'}
複製代碼

這樣咱們就保證了ajax函數的乾淨,提升了ajax函數的複用性,而且也知足了添加token的需求。

插件式的表單驗證

表單驗證在web開發中是一個很常見的需求,好比在一個登陸頁面,咱們在把用戶的數據,好比用戶名、密碼等信息提交給服務器以前,就會常常須要作校驗,假設咱們如今只須要校驗字段是否爲空,因而有以下代碼:

var username = document.getElementById('username')
var password = document.getElementById('password')
var submitBtn = document.getElementById('submitBtn')

function submitHandler () {
  if (username.value === '') {
    return alert('用戶名不能爲空')
  }
  if (password.value === '') {
    return alert('密碼不能爲空')
  }
  ajax('post', 'http://xxx.com/login', { username: username.value, password: password.value })
}

submitBtn.onclick = submitHandler
複製代碼

上面的submitHandler在此處承擔了兩個職責,除了ajax的請求以外,還要驗證用戶輸入的合法性,這種函數首先一旦校驗的字段不少,代碼就會臃腫,並且函數職責也很混亂,沒法複用。 下面使用AOP進行優化,首先分離校驗相關的代碼:

function validateField () {
   if (username.value === '') {
    alert('用戶名不能爲空')
    return false
  }
  if (password.value === '') {
    alert('密碼不能爲空')
    return false
  }
}

function submitHandler () {
  var params = { username: username.value, password: password.value }
  ajax('post', 'http://xxx.com/login', params)
}
複製代碼

改寫前面的Function.prototype.before:

Function.prototype.before = function (beforeFn) {
  var self = this

  return function () {
    const ret = beforeFn.apply(this, arguments)
    if (ret === false) {
      return
    }

    return self.apply(this, arguments)
  }
}
複製代碼

再用validateField前置裝飾submitHandler:

submitHandler = submitHandler.before(validateField)

submitBtn.onclick = submitHandler
複製代碼

這樣咱們就完美將校驗的代碼和提交ajax請求的代碼徹底分離開來,它們再也不有耦合關係,這樣咱們在項目中能夠把一些校驗函數封裝起來,達到複用的目的。

裝飾者模式和代理模式

裝飾者模式和代理模式的結構看起來很像,這兩種模式都描述了怎麼樣爲對象提供必定程度上的間接引用,它們的實現部分保留了對另外一個對象的引用,而且客戶是直接向那個對象發送請求。

代碼模式和裝飾者模式最重要的區別是在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便時或者不合符需求時,爲本體提供一個替代者。本體定義了核心的功能,而代理提供的做用一個是直接拒絕一些訪問,另外一個就是在本體以前作一些額外的事情。而裝飾者的做用是給對象動態添加行爲,能夠說代理模式強調一種本體和代替者的一種能夠靜態表達的關係,這種關係在一開始就基本被肯定了。而裝飾者模式一開始並不能肯定全部的功能,在不一樣的場景中,可能會根據須要添加不一樣的裝飾者,這些裝飾者能夠造成一條長長的裝飾鏈。

總結

經過上面的三個應場景:數據上報、動態改變函數參數以及表單校驗,咱們能夠看到在JavaScript中,咱們瞭解了裝飾函數,瞭解了AOP,他們就是JavaScript中獨特的裝飾者模式,這種模式在實際開發中很是有用。

相關文章
相關標籤/搜索