裝飾者模式能夠動態地給某個對象添加一些額外的職責,而不會影響這個類中派生的其餘對象。裝飾模式可以在不改變對象自身的基礎上,在程序運行期間給對象動態地添加職責,跟繼承相比,裝飾者更加輕便靈活。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實現上面例子的代碼以下: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)
}
複製代碼
這樣的代碼是符合開放-封閉原則的,咱們增長新的功能的時候,沒有修改原來的代碼。但這種方式有一些缺點:異步
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')
複製代碼
首先定義兩個函數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的經典應用之一。在項目的開發結尾的時候,咱們通常須要加一些統計數據的代碼,這些過程可能讓咱們被迫改動已經封裝好的函數。好比,頁面中有登陸按鈕,點擊登陸按鈕,彈出登陸彈窗的同時還要上報數據,來統計有多少用戶點擊了登陸按鈕。下面簡單的代碼實現:
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
複製代碼
觀察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中獨特的裝飾者模式,這種模式在實際開發中很是有用。