JavaScript装饰者模式

402 阅读5分钟

本文是《JavaScript设计模式与开发实践》的学习笔记,例子来源于书中,对于设计模式的看法,推荐看看本书作者的建议

什么是装饰者模式?

给对象动态增加职责的方式成为装饰者模式。

装饰者模式能够在不改变对象自身的基础上,在运行程序期间给对象动态地添加职责。这是一种轻便灵活的做法,装饰者是一种“即付即用”的方式,比如天冷了就多穿一件外套。

装饰函数

想要为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但是这是最差的办法,直接违反了开放——封闭原则。

var a = function(){
    alert(1)
}
// 改成
var a = function(){
    alert(1)
    alert(2)
}

很多时候我们不想碰原函数,也许原函数是其他同事编写的,甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿触碰的阴暗角落里。现在需要不改变源代码的情况下,给函数增加功能。

我们通过保存原引用的方式改写某个函数。

var a = function(){
    alert(1)
}
var _a = a
a = function(){
    _a()
    alert(2)
}
a()

这是实际开发中很常见的一个做法,比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,先保存 window.onload,把它放入新的 window.onload。

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语句预测用户的实际需要。