装饰者模式
装饰者模式定义
装饰者模式是指在不改变原对象的基础上,通过对其进行包装拓展(添加属性或方法)使原有对象可以满足客户更复杂的需求。
在开发过程中,有时候想要为函数添加一些功能,最简单粗暴的方法就是直接改写该函数,但这是最差的方法,违反了开放-封闭原则。而且,很多时候我们不想去修改原函数,比如我们想给window.onload事件绑定一个方法,但不确定这个事件是否已经被其他人绑定过,为了避免覆盖之前的window.onload函数中的行为,我们可以先把原先的window.onload保存好,把它放入新的window.onload执行:
window.onload = function() {
//原有的代码逻辑
...
}
var _load = window.onload || function(){};
window.onload = function() {
//先执行原有的逻辑
_load();
//再执行新的逻辑
...
}
AOP
上面window.onload的方法修改后还存在两个问题:
- 需要维护_load这个中间变量,如果函数的装饰链比较长或者需要装饰的函数变多,这些中间变量的数量也会变多
- 可能遇到this被劫持的问题。在window.onload的例子里面没有这个问题,因为调用普通函数_load时this也指向window,跟调window.onload一样(函数作为对象的方法被调用时,this指向该对象),如果现在把window.onload换成document.getElementById:
var _getElementbyId = document.getElementById;
document.getElementById = function(id) {
...
return _getElementbyId(id);
}
var btn = document.getElementById("btn");//Uncaught TypeError: Illegal invocation
此时,_getElementbyId 是一个全局函数,当调用一个全局函数时this指向window,因此报错。我们可以手动吧document的上下文this传入_getElementbyId
document.getElementById = function(id) {
...
return _getElementbyId.apply(document,arguments);
}
这样做显然不方便,我们可以通过AOP来提供一种完美的方法给函数动态增加功能。
AOP装饰函数
Function.prototype.before
Function.prototype.before = function(beforeFn) {
var _self = this;
return function() {
beforeFn.apply(this,arguments);
return _self.apply(this,arguments);
}
}
Function.prototype.before方法首先保存了原函数的引用,然后返回了一个包含原函数和新函数的代理函数,在这个函数里,先执行新函数,并且保证this不被劫持,新函数接受的参数也会被原封不动地传入原函数,然后再执行原函数并返回原函数的执行结果,这样就实现了动态装饰的效果。
document.getElementById = document.getElementById.before(function(){...});
var btn = document.getElementById("button");
Function.prototype.after
Function.prototype.after = function(afterFn) {
var _self = this;
return function() {
var ret = _self.apply(this,arguments);
afterFn.apply(this,arguments);
return ret;
}
}
Function.prototype.after的原理和Function.prototype.before一样,不过是让新添的函数在原函数之后再执行。
AOP应用
用AOP动态改变函数的参数
在before方法中,新增函数和原函数使用的参数都是共用一组参数列表arguments,如果我们在新增函数beforeFn中改变了arguments,原函数接收的参数列表也会改变。
var fn = function(param){console.log(param);} //{a:'a',b:'b'}
fn.before(function(){param.b = 'b';});
fn({a:'a'})
插件式表单验证
<body>
<form action="">
用户名<input type="text" name="username" id="username">
密码<input type="text" name="password" id="password">
<button id="submit-btn" >提交</button>
</form>
<script>
Function.prototype.before = function(beforeFn){
let _this = this;
return function(){
let ret = beforeFn.apply(this,arguments);
if(ret)
_this.apply(this,arguments);
}
}
function submit(){
alert('提交表单');
}
submit= submit.before(function(){
let username = document.getElementById('username').value;
if(username.length<6){
return alert('用户名不能少于6位');
}
return true;
});
submit = submit.before(function(){
let username = document.getElementById('username').value;
if(!username){
return alert('用户名不能为空');
}
return true;
});
document.getElementById('submit-btn').addEventListener('click',submit);
</script>
</body>
小结
装饰者模式VS继承
在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来问题:
- 会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变
- 继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性
- 在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如现在有4种型号的自行车,如果要为每种自行车都定义一个单独的类,,现在要给每种自行车装上铃铛,尾座两种配件,使用继承的方式来给每种自行车创建子类,则需要2*4=8个子类。但使用装饰者模式,只需额外增加两个类将铃铛,尾座两个对象动态组合到自行车上。
装饰者模式VS代理模式
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合要求的时候为这个本体提供一个替代者。本体定义了关键功能,而代理提供或者拒绝它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用是为对象动态加入行为。
AOP优化
如果不希望通过原型的方式来定义before和after的方法,可以将原函数和新函数都作为参数传入before和after方法:
function before(fn,beforeFn) {
return function(){
beforeFn.apply(this,arguments);
return fn.apply(this,arguments);
}
}
function after(fn,afterFn) {
return function(){
var ret = fn.apply(this,arguments);
afterFn.apply(this,arguments);
return ret;
}
}