给对象动态地增加职责的方式称为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式。
假设我们在编写一个飞机大战的游戏,随着经验值的增加,飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。
// 首先是原始的飞机类
var Plane = function(){}
Plane.prototype.fire = function(){
console.log("发射普通子弹");
}
// 增加两个装饰类,分别是导弹和原子弹
var MissileDecorator = function(plane){
this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
this.plane.fire();
console.log("发射导弹");
}
var AtomDecorator = function(plane){
this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
this.plane.fire();
console.log("发射原子弹");
}
// 测试一下:
var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();
导弹类和原子弹类的构造函数都接受参数plane对象,并且保存好这个参数,在它们的fire方法中,除了执行自身的操作之外,还调用plane对象的fire方法。
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
我们可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式:
var plane = {
fire:function(){
console.log("发射普通子弹");
}
}
var missileDecorator = function(){
console.log("发射导弹");
}
var atomDecorator = function(){
console.log("发射原子弹");
}
var fire1 = plane.fire;
plane.fire = function(){
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
fire2();
atomDecorator();
}
plane.fire();
要想为函数添加一些功能,最简单粗暴的方式就是直接改写该函数,但这是最差的办法,直接违反了开放-封闭原则。
比如:
//将
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 = function(){
_onload();
alert(2)
}
上面的代码符合开闭原则,但是存在两个问题:
var _getElementById = document.getElementById;
document.getElementById = function(id){
alert(1);
return _getElementById(id);
}
var button = document.getElementById('button');
可以看到抛出异常:
此时_getElementById是一个全局函数,当调用一个全局函数时,this是指向window的,而document.getElementById方法的内部实现需要使用this引用,this在这个方法内预期是指向document,而不是window,这是错误发生的原因。
改进一下代码,我们要手动把document当作上下文this传入_getElementById:
var _getElementById = document.getElementById;
document.getElementById = function(){
alert(1);
return _getElementById.apply(document,arguments);
}
var button = document.getElementById('button');
这种形式还不太方便,接下来看一下AOP装饰函数。
首先了解Function.prototype.before
和Function.prototype.after
方法:
Function.prototype.before = function(beforefn){
var _self = this;//保存原函数的引用
return function(){//返回包含了原函数和新函数的“代理”函数
//执行新函数,且保证this不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
beforefn.apply(this,arguments);
//执行原函数并返回原函数的执行结果,并且保证this不被劫持
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;
}
}
Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。
接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理,并不承担代理的职责,它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。
接下来试一下:
<button id="button"></button>
document.getElementById = document.getElementById.before(function(){
alert(111);
});
var button = document.getElementById('button');
console.log(button)
在window.onload的例子基础上使用after函数:
window.onload = function(){
alert(1)
};
window.onload = (window.onload || function(){}).after(function(){
alert(2);
}).after(function(){
alert(3)
}).after(function(){
alert(4)
})
有些人不喜欢这种污染原型的方式,我们可以把原函数和新函数都作为参数传入before或者after的方法:
var before = function(fn,beforefn){
return function(){
beforefn.apply(this,arguments);
return fn.apply(this,arguments)
}
}
var a = before(
function(){alert(2)},
function(){alert(3)}
);
a = before(a,function(){alert(1);});
a();
1.数据统计上报
分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一。
比如页面中有一个登录button,点击这个button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button:
<button tag="login" id="button">点击打开登录浮层</button>
var showLogin = function(){
console.log('打开登录浮层');
log(this.getAttribute('tag'));
}
var log = function(tag){
console.log('上报标签为:'+tag);
}
document.getElementById('button').onclick = showLogin;
打开登录浮层和数据上报被耦合在一个函数里,使用AOP分离之后:
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;
2.用AOP动态改变函数的参数
Function.prototype.before = function(beforefn){
var _self = this;//保存原函数的引用
return function(){//返回包含了原函数和新函数的“代理”函数
//执行新函数,且保证this不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
beforefn.apply(this,arguments);
//执行原函数并返回原函数的执行结果,并且保证this不被劫持
return _self.apply(this,arguments);
}
}
var func = function(param){
console.log(param);
}
func = func.before(function(param){
console.log("111",this)
param.b = 'b'
})
func({'a':'a'})//{a: 'a', b: 'b'}
现在有一个用于发起ajax请求的函数,这个函数负责项目中所有的ajax异步请求:
var ajax = function(type,url,param){
console.dir(param);
// 发送ajax请求的代码
}
ajax('get','http://xxx.com/userinfo',{name:'sven'});
现在需要在HTTP请求中带一个Token参数:
var getToken = function(){
return 'Token';
}
// 现在的任务是给每一个ajax请求都加上Token参数:
var ajax = function(type,url,param){
param = param || {};
param.Token = getToken();
console.dir(param);
}
ajax('get','http://xxx.com/userinfo',{name:'sven'});
虽然解决了问题,但我们的ajax函数相对变得僵硬了,如果另一个项目不需要验证Token,或者是Token的生成方式不同,都必须重新修改ajax函数。
为了解决这个问题,先把ajax函数还原成一个干净的函数:
var ajax = function(type,url,param){
console.dir(param);//{Token: "Token",name: "sven"}
// 发送ajax请求的代码
}
// 把Token参数通过before装饰到ajax函数的参数param对象中:
var getToken = function(){
return 'Token';
}
// 现在的任务是给每一个ajax请求都加上Token参数:
ajax = ajax.before(function(type,url,param){
param.Token = getToken();
})
ajax('get','http://xxx.com/userinfo',{name:'sven'});
明显可以看到,用AOP的方式给ajax函数动态装饰上Token参数,保证了ajax函数是一个相对纯净的函数,提高了ajax函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。
3.插件式的表单验证
表单验证对我们来说并不陌生,我们经常要在表单数据提交之前做一下校验,比如登录的时候需要验证用户名和密码是否为空:
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
}
// 提交代码略
console.log("代码提交");
}
submitBtn.onclick = function(){
formSubmit();
}
此时的formSubmit有两个职责,提交和验证,这样的代码比较臃肿,职责混乱。
接下来分离校验输入和提交:
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
}
// 提交代码略
console.log("代码提交");
}
submitBtn.onclick = function(){
formSubmit();
}
接下来进一步优化这段代码,使validata和formSubmit完全分离开来。先改写Function.prototype.before,如果beforefn的执行结果返回false,表示不再执行后面的原函数:
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
}
// 提交代码略
console.log("代码提交");
}
formSubmit = formSubmit.before(validata);
submitBtn.onclick = function(){
formSubmit();
}
这段代码中,校验输入和提交表单的代码完全分离开来。