JavaScript 装饰者模式
前言
装饰者模式是一种经典的设计模式,它允许向对象动态地添加额外的功能。这种模式通过创建装饰类来包裹原始类的实例,从而达到增强原始类功能的目的,同时不影响原始类的结构。
装饰者模式在设计之初是想称为包装器模式的,因为从结构上看,这种说法更加符合它,通过一个对象包裹另一个对象,甚至多个对象进行嵌套。
在JavaScript中实现一个装饰者模式是非常简单的一件事,因为是一本动态语言,它都不用像java那样需要写一个类来包装源对象,最简单的做法就是直接改写对象的原属性,在实际的代码中,常常用函数来进行实现装饰器模式。
var obj = {
name: 'sven',
address: '深圳市'
};
obj.address = obj.address + '福田区'; // 装饰者模式简单实现
仿静态类型语言的实现
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(); // 分别输出: 发射普通子弹、发射导弹、发射原子弹
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象 之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire 方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
JavaScript的装饰器实现
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(); // 分别输出: 发射普通子弹、发射导弹、发射原子弹
我们可以直接改写对象的属性而不需要使用类来实现,通过复写对象的方法,且新方法里保留对原方法的调用。
但是这种方式也有问题,第一是this的问题,其次是每次都要在新增的函数中耦合上一个函数,甚至有时候可能上一个函数并不存在,如果直接运行还会导致报错。
首先解决上一个函数可能不存在的问题,大部分的做法就是增加一个默认值:
window.onload = function() {
alert(1);
}
var _onload = window.onload || function() {};
window.onload = function() {
_onload();
alert(2);
}
然后就是this和参数的问题:
var _getElementById = document.getElementById;
document.getElementById = function() {
alert(1);
return _getElementById.apply(document, arguments);
}
var button = document.getElementById('button');
我们通过apply
来解决this的指向问题,通过arguments
传递参数。
解决了上面两个问题,还有一个问题就是新函数与旧函数的耦合问题,每次新增一个函数,在新函数体内就得维护一个旧函数的指向,这显然不符合开发封闭原则。
解决的办法也有,就是通过一个外层的工具来处理,常见的做法就是AOP装饰函数。
AOP装饰函数
Function.prototype.before = function(beforefn) {
var __self = this; // 保存原函数的引用
return function() { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
}
}
Function.prototype.after = function(afterfn) {
var __self = this;
return function() {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
通过工具函数,在工具函数里面持有旧函数的引用,工具函数再负责函数的执行,这样耦合部分与新增的函数业务实现的解耦,我们就不需要在新函数里关心旧函数了。
document.getElementById = document.getElementById.before(function() {
alert(1);
});
var button = document.getElementById('button');
console.log(button);
我们通过before
在运行原函数之前运行了一个弹窗通知,然后再运行原函数,工具函数return了原函数的结果,所以打印button也是没有问题的(不影响原来的功能)。
再看一个例子:
window.onload = function() {
alert(1);
}
window.onload = (window.onload || function() {}).after(function() {
alert(2);
}).after(function() {
alert(3);
}).after(function() {
alert(4);
});
不影响原型链的AOP实现
有时候我们可能不想影响原型链,可以通过封装一个函数来实现:
var before = function(fn, beforefn) {
return function() {
beforefn.apply(this, arguments);
return fn.apply(this, arguments);
}
}
var a = before(
function() {
alert(3)
},
function() {
alert(2)
}
);
a = before(a, function() {
alert(1);
});
a();
AOP实际应用
后端常常用aop来实现日志的打印上报,有时候我们也可以通过aop来动态改变函数的参数:
Function.prototype.before = function(beforefn) {
var __self = this;
return function() {
beforefn.apply(this, arguments); // (1)
return __self.apply(this, arguments); // (2)
}
}
var func = function(param) {
console.log(param); // 输出: {a: "a", b: "b"}
}
func = func.before(function(param) {
param.b = 'b';
});
func({
a: 'a'
});
在表单的校验中也可以使用AOP:
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();
}
可以看到在这个表单替换的函数中存在了表单校验的代码,它和具体的提交表单是两种不同的业务逻辑,从单一原则上来讲,这个代码就不符合。
如果校验规则过多,这个函数也会变得庞大,于是我们常见的一种优化方式就是将校验提取出来,然后通过return布尔值来处理判断。
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();
}
可以看到这种方式好了很多,但是提交表单里面还是需要耦合validata
的代码,我们可以利用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();
}
通过这种方式代码完全解耦了,如果哪天不需要表单校验了,删除formSubmit = formSubmit.before(validata);
也不会影响提交的函数formSubmit
。
注意事项
AOP的方式使得我们如果在原函数上保存的一些属性会失效,因为返回的是新函数。
var func = function() {
alert(1);
}
func.a = 'a';
func = func.after(function() {
alert(2);
});
alert(func.a); // 输出:undefined
如果这种装饰方式嵌套过多也会有问题,因为函数作用域的叠加导致一些性能问题。
区分装饰者模式和代理模式
虽然那装饰者模式在定义上也是通过一个新的包装对象来访问,但是和代理模式还是不同的,我们要从它们的意图和设计目的进行区分。
代理模式是在原对象不方便访问的情况下,通过一个代理对象来帮助处理,代理对象和原对象往往是1对1的关系。
而装饰者模式是为了扩展对象,在不影响原功能的情况下增加更多的功能,且是一个可以叠加效果的(嵌套)。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据