JavaScript裝飾者模式

本文是《JavaScript設計模式與開發實踐》的學習筆記,例子來源於書中,對於設計模式的見解,推薦看看本書做者的建議html

什麼是裝飾者模式?

給對象動態增長職責的方式成爲裝飾者模式。ajax

裝飾者模式可以在不改變對象自身的基礎上,在運行程序期間給對象動態地添加職責。這是一種輕便靈活的作法,裝飾者是一種「即付即用」的方式,好比天冷了就多穿一件外套。編程

裝飾函數

想要爲函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,可是這是最差的辦法,直接違反了開放——封閉原則。設計模式

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

不少時候咱們不想碰原函數,也許原函數是其餘同事編寫的,甚至在一個古老的項目中,這個函數的源代碼被隱藏在一個咱們不肯觸碰的陰暗角落裏。如今須要不改變源代碼的狀況下,給函數增長功能。安全

咱們經過保存原引用的方式改寫某個函數。bash

var a = function(){
    alert(1)
}
var _a = a
a = function(){
    _a()
    alert(2)
}
a()
複製代碼

這是實際開發中很常見的一個作法,好比咱們想給 window 綁定 onload 事件,可是又不肯定這個事件是否是已經被其餘人綁定過,爲了不覆蓋掉以前的 window.onload 函數中的行爲,先保存 window.onload,把它放入新的 window.onload。app

window.onload = function(){
    alert(1)
}

var _onload = window.onload || function(){}

window.onload = funtion(){
    _onload()
    alert(2)
}
複製代碼

這樣的代碼是符合封閉——開放原則,咱們在增長新功能的時候確實沒有修改原來的代碼,可是這種方式存在兩個問題:框架

  1. 必須維護 _onload 這個中間變量,雖然看起來不起眼,可是若是函數裝飾鏈較長,或者須要裝飾的函數變多,這些中間變量的數量也會愈來愈多。
  2. 其實還遇到了 this 被劫持的問題,在 window.onload 的例子中沒有這個煩惱,由於調用 _onload 的時候 this 也指向 window,跟調用 window.onload 的時候同樣。

用 AOP 裝飾函數

AOP(Aspect Oriented Programming)面向切面編程的主要做用是:把一些跟核心業務邏輯無關的功能抽離出來,這些跟業務邏輯無關的功能一般包括日誌統計、安全控制、異常處理等。把這些功能抽離出來之後,再經過「動態織入」的方式摻入業務邏輯模塊中。這樣的好處首先是能夠保持業務邏輯模塊的純淨和高內聚性,其次是能夠很方便地複用日誌統計等功能模塊。函數

首先給出 Function.prototype.before 和 Function.prototype.after 方法:學習

Function.prototype.before = function(beforefn){
    // 保存原函數的引用
    var _self = this
    // 返回包含原函數和新函數的「代理函數」
    return function(){
        // 執行新函數,保證this不被劫持,
        // 新函數接受的參數也會被原封不動地傳入原函數,
        // 新函數在原函數以前執行
        beforefn.apply(this,arguments)
        return _self.apply(this.arguments)
    }
}

Function.prototype.after = function(afterfn){
    var _self = this
    return function(){
        var ret = _self.apply(this,arguments)
        afterfn.apply(this,arguments)
        return ret
    }
}
複製代碼

「代理函數」只是結構上像代理而已,並不承擔代理的職責(好比控制對象的訪問),它的工做是把請求分別轉發給新添加的函數和原函數,且負責保證它們的執行順序。

再回到 window.onload 的例子中,用 Function.prototype.after 來增長新事件:

window.onload = function(){
    alert(1)
}

window.onload = (window.onload || function(){}).after(function(){
    alert(2)
}).after(function(){
    alert(3)
})
複製代碼

AOP 的應用實例

  1. 數據統計上報
<html>
    <button tag="login" id="button">點擊打開登陸浮層</button>
    <script>
        var showLogin = function(){
	    console.log( '打開登陸浮層' )
	    log( this.getAttribute( 'tag' ) )
	}
        var log = function( tag ){
            console.log( '上報標籤爲: ' + tag )
	}
	document.getElementById( 'button' ).onclick = showLogin
    </script>
</html>
複製代碼

showLogin 函數既要負責打開浮層,又要負責數據上報,兩個功能耦合在一個函數裏,使用 AOP 分離:

<html>
<button tag="login" id="button">點擊打開登陸浮層</button>
<script>
    Function.prototype.after = function( afterfn ){
        var __self = this;
        return function(){
        var ret = __self.apply( this, arguments )
        afterfn.apply( this, arguments )
        return ret
        }
    }
    var showLogin = function(){
        console.log( '打開登陸浮層' )
    }
    var log = function(){
        console.log( '上報標籤爲: ' + this.getAttribute( 'tag' ) )
    }

    showLogin = showLogin.after( log ); // 打開登陸浮層以後上報數據
    document.getElementById( 'button' ).onclick = showLogin;
</script>
</html>
複製代碼
  1. 插件式表單驗證
<html>
<body>
    用戶名:<input id="username" type="text"/>
    密碼: <input id="password" type="password"/>
    <input id="submitBtn" type="button" value="提交"></button>
</body>
<script>
    var username = document.getElementById( 'username' ),
    password = document.getElementById( 'password' ),
    submitBtn = document.getElementById( 'submitBtn' );
    var formSubmit = function(){
        if ( username.value === '' ){
            return alert ( '用戶名不能爲空' );
        }
        if ( password.value === '' ){
            return alert ( '密碼不能爲空' );
        }
        var param = {
            username: username.value,
            password: password.value
        }
        ajax( 'http:// xxx.com/login', param ); // ajax 具體實現略
    }

    submitBtn.onclick = function(){
        formSubmit();
    }
</script>
</html>
複製代碼

formatSubmit 函數承擔了兩個職責,除了提交ajax請求,還要驗證用戶輸入的合法性。咱們把校驗輸入的邏輯放到validata函數中,並約定當validata函數返回false的時候表示校驗未經過。

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

var formSubmit = function(){
    if ( validata() === false ){ // 校驗未經過
	return;
    }
    var param = {
        username: username.value,
        password: password.value
    }
    ajax( 'http:// xxx.com/login', param );
}

submitBtn.onclick = function(){
    formSubmit();
}
複製代碼

使用AOP優化代碼

Function.prototype.before = function( beforefn ){
    var __self = this;
    return function(){
    if ( beforefn.apply( this, arguments ) === false ){
    // beforefn 返回false 的狀況直接return,再也不執行後面的原函數
        return;
    }
    return __self.apply( this, arguments );
    }
}

var validata = function(){
    if ( username.value === '' ){
        alert ( '用戶名不能爲空' );
        return false;
    }
    if ( password.value === '' ){
        alert ( '密碼不能爲空' );
        return false;
    }
}
var formSubmit = function(){
    var param = {
        username: username.value,
        password: password.value
    }
    ajax( 'http:// xxx.com/login', param );
}

formSubmit = formSubmit.before( validata );
    submitBtn.onclick = function(){
        formSubmit();
}
複製代碼

做者提出,除了上面的兩個例子,在開發框架的時候也十分有用。框架裏的函數提供的是穩定而方便移植的功能,個性化的功能能夠在框架以外動態裝飾上去,能夠避免爲了讓框架有更多功能而去使用一些if、else語句預測用戶的實際須要。

相關文章
相關標籤/搜索