JavaScript设计模式笔记-装饰者模式

最常见的装饰者模式

比如我们要给onload增加新的方法,我们又不能改动内部的代码,如果改动了那么就违反了开放-封闭原则

1
2
3
4
5
6
7
8
window.onload=function(){
alert(1)
}
var _onload=window.onload||function(){};
window.onload=function(){
_onload();
alert(2);
}

当然这个函数还是存在问题的

  • 必须要维护_onload这个中间变量,如果后续添加的方法越来越多,那么中间变量也就会越来越多
  • this丢失问题

比如我们把上述的代码换成document.getElementById

1
2
3
4
5
6
var _getElementById=document.getElementById;
document.getElementById=function(id){
alert(1)
return _getElementById(id)
}

很明显这段代码执行起来会有问题,因为这里的_getElementById(id)中this指向的并不是document这个对象而是全局的window
那么我们可以用call来修复,但是这样做明显不方便

1
2
3
4
5
6
var _getElementById=document.getElementById;
document.getElementById=function(id){
alert(1)
return _getElementById.call(document,id)
}

于是就有一门新的方法 AOP面向切片

AOP装饰函数

首先来写Function.prototype.after和Function.prototype.before

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.after=function(afterFn){
var self=this //保存原函数引用
return function(){
var ret=self.apply(this,arguments)//执行原函数
afterFn.apply(this,arguments)//执行新函数,并且保证this不会被劫持
return ret//返回原函数结果
}
}
Function.prototype.before=function(beforeFn){
var self=this
return function(){
beforeFn.apply(this,arguments)
var ret=self.apply(this,arguments)
return ret
}
}

上面的代码很简单.一个是先执行原函数一个是先执行新函数.下面来看一段代码

1
2
3
<button id="button">button</button>
document.getElementById=document.getElementById.before(function(){alert(1)});
var button=document.getElementById("button")

先会弹出一个1然后再获取到button.包括改写window.onload这个例子

1
2
3
4
window.onload=function(){
alert(1)
}
window.onload=(window.onload||function(){}).after(function(){alert(2)}).after(function(){alert(3)})

如果不喜欢这种污染了原型的AOP那么这里可以给出通用的AOP

1
2
3
4
5
6
7
8
9
var before=function(fn,before){
return function(){
before.apply(this,arguments)
return fn.apply(this,arguments)
}
}
var a=before(function(){alert(3)},function(){alert(2)})
a=before(a,function(){alert(1)})

数据统计上报

当点击按钮后打开登陆浮窗,然后再进行数据上报

1
2
3
4
5
6
7
8
9
10
11
12
var showLogin=function(){
console.log("打开登陆浮窗")
}
var log=function(){
console.log("数据上报"+this.getAttribute("tag"))
}
showLogin.after(log)
document.getElementById("button").onclick=showLogin

AOP动态修改传递参数

比如我们需要在ajax的时候传递一个token参数,但是这个token非必须的,即有些链接不需要这个token参数.那么这里就可以用AOP来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
var ajax=function(type,url,param){
console.log(param)
};
var getToken=function(){
return "Token"
}
ajax=ajax.before(function(type,url,param){
param.token=getToken()
})
ajax("get","http://xxx.com/x",{name:"sven"})

这里的动态传递主要取决于AOP的这条语句

1
2
3
4
return function(){
before.apply(this,arguments)
return fn.apply(this,arguments)
}

当我们使用AOP的时候实际上返回的就是这个function.而先执行before函数的时候 因为修改了param所以导致执行fn.apply(this,arguments)语句的时候arguments值也变了.

插件式表单验证

当我们点击表单按钮的时候验证是否通过.要避免那些大量的if else语句那么我们可以用装饰者模式,只需要修改一个地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Function.prototype.before=function(beforeFn){
var self=this
return function(){
if(beforeFn.apply(this,arguments)===false){//如果验证不通过直接返回false
return
}
var ret=self.apply(this,arguments)
return ret
}
}
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/x",param)
}
formSubmit.before(validata)//提交之前先进行验证
submitBtn.onclick=function(){
formSubmit();
}

AOP这个符合了开放-封闭原则,在不修改源代码的模式下如何去增加新的功能,但是要注意的是 如果你直接给function增加了一些属性,那些属性在AOP后是不存在的 例如

1
2
3
4
5
6
7
8
var func=function(){
alert(1);
}
func.a="a"
func=func.after(function(){
alert(2)
})
alert(func.a);//undefined

总结

装饰者模式与代理模式比较想像,但是不同的是 代理模式一开始就确定了代理与本体的关系,装饰者模式是在不清楚本体是谁的情况下提供的一个代理.
代理模式通常只有一层代理-本体引用,而装饰者模式经常会形成一条长长的装饰链.