【专题】装饰者模式
在传统的面向对象语言中,经常使用继承的方式给对象添加方法。但是继承的方式往往会引起一些问题。一是父类与子类之间强耦合,二是破坏原有的封装性。而装饰者模式能够在不改变对象自身的基础上,动态地给对象添加属性。
简单实现js中的装饰者
动态的修改对象的属性很简单,这样就可以做到了,
let obj = {
name: 'hello',
age: 123,
};
obj.name = obj.name + ' world';
当然装饰者模式不会改变原对象的,我们在此基础之上做修改,
let first = {
print: function() {
console.log('first');
}
};
let secondPrint = function() {
console.log('second');
};
let thirdPrint = function() {
console.log('third');
};
let print1 = first.print;
first.print = function() {
print1();
secondPrint();
}
let print2 = first.print;
first.print = function() {
print2();
thirdPrint();
}
first.print(); // 以此按行输出:first,second,third
我们通过保存原先函数引用的方式就可以重写之前的某个函数。其实这种方式很常见,像DOM0级事件绑定时,假如我们要给window对象绑定load回调事件,
// 假如某个js里有定义
window.onload = function() {
console.log(1);
}
// 正在开发的js文件
let _onload = window.onload || function() {};
window.onload = function() {
_onload();
console.log(2);
}
由于我们不能确定window.onload是否在其他页面被定义,为了防止他被覆盖,于是先保存原先的onload事件,再将其放入新定义的onload中执行。
虽然达到了效果,但是这种方式有几个问题:
- 中间变量 _onload 要维护,如果要装饰的函数变多,要维护的中间变量也会变多。
- this丢失问题,当然这儿没有,因为执行环境指向的window。
考虑下以下代码:
let _document = document.getElementById;
document.getElementById = function(id) {
console.log(1);
return _document(id);
};
document.getElementById('btn');
由于调用全局的 _document 方法时,this是指向window的,而该方法需要指向document才能作用,所以出错了。
所以调用时需要改变下this指向,借助apply或call,
// ...
return _document.apply(document, [id]);
// ...
AOP修饰函数
首先给出before和after两个方法,
Function.prototype.before = function(beforFn) {
let _self = this; // 原函数引用
return function() {
beforFn.apply(this, arguments);
return _self.apply(this, arguments);
};
}
Function.prototype.after = function(afterFn) {
let _self = this; // 原函数引用
return function() {
let res = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return res;
};
}
两函数都接收一个新功能函数,唯一区别则是该新功能是前置执行,还是后置执行。而apply的使用保证this指向不会被丢失。
当然直接给原型增加新方法会污染原型,将这两方法稍作修改:
const before = function(fn, beforeFn) {
return function() {
beforeFn.apply(this, arguments);
return fn.apply(this, arguments);
}
};
const after = function(fn, afterFn) {
return function() {
let res = afterFn.apply(this, arguments);
fn.apply(this, arguments);
return res;
}
};
let a = before(function() {
console.log(111);
}, function() {
console.log(222);
});
a = before(a, function() {
console.log(333);
});
a(); // 333 222 111
应用实例
动态修改参数
假如我们现在实现了一个简单的ajax请求,
let ajax = function(type, url, params) {
// ... ajax处理
console.log(params);
};
现在由于安全问题,现在需要在params增加个token参数,利用前面的Function.prototype.before装饰到ajax函数中的params参数上,
let getToken = function() {
return 'token';
};
ajax = ajax.before(function(type, url, params) {
params.token = getToken();
});
ajax('get', 'http://xx.com', { name: 'hello' }); // {name: "hello", token: "token"}
利用AOP方式给ajax函数动态地增加了参数,也保证了ajax的独立性,提高了ajax函数的复用性。
插件式表单验证
表单是web上必不可少的一部分,那么他的验证也是必不可少的。比如登录模块,至少需要检验用户名和密码是非空,
<body>
<input type="text" id="username" />
<input type="password" id="password" />
<input type="button" id="btn" value="提交" />
</body>
<script>
let oUser = document.getElementById('username'),
oPass = document.getElementById('password'),
oBtn = document.getElementById('btn');
let submit = function() {
if (oUser.value === '') {
return console.log('用户名不能为空');
}
if (oPass.value === '') {
return console.log('密码不能为空');
}
const params = {
username: oUser.value,
password: oPass.value,
}
ajax('...', params); // 简略
};
oBtn.onclick = function() {
submit();
};
</script>
submit函数此处需要负责数据合法校验和提交ajax,造成函数臃肿,职责混乱,没有复用性。首先将校验部分代码分离,
const validate = function() {
if (oUser.value === '') {
console.log('用户名不能为空');
return false;
}
if (oPass.value === '') {
console.log('密码不能为空');
return false;
}
};
let submit = function() {
if (validate() === false) {
return;
}
// ....
};
代码已经有了改进,虽然校已经独立到validate函数中了,但是submit函数仍要计算validate函数的返回值,因为返回值表明了是否通过校验。
理好先后顺序,是优先validata后,再到submit,略微修改before函数:
Function.prototype.before = function(beforFn) {
let _self = this; // 原函数引用
return function() {
if (!beforFn.apply(this, arguments)) {
// 验证失败,不通过
return;
}
return _self.apply(this, arguments);
};
}
const validate = function() {
if (oUser.value === '') {
console.log('用户名不能为空');
return false;
}
if (oPass.value === '') {
console.log('密码不能为空');
return false;
}
};
let submit = function() {
const params = {
username: oUser.value,
password: oPass.value,
};
ajax('...', params); // 简略
}
submit = submit.before(validate);
oBtn.onclick = function() {
submit();
};
现在将校验和提交ajax完全拆分开了,不存在耦合关系,这样要增加其他校验规则,只需要动validate就行了。
当然,由于function也是对象,但装饰返回的是一个新函数,所以原函数上的属性就会丢失。
总结
装饰者模式在平时的开发过程中用的非常多,当然我们要注意下装饰太多的话,性能还是会有影响的。
参考《JavaScript设计模式与开发实践》