前言

装饰者模式是一种经典的设计模式,它允许向对象动态地添加额外的功能。这种模式通过创建装饰类来包裹原始类的实例,从而达到增强原始类功能的目的,同时不影响原始类的结构。

装饰者模式在设计之初是想称为包装器模式的,因为从结构上看,这种说法更加符合它,通过一个对象包裹另一个对象,甚至多个对象进行嵌套。

在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的关系。

而装饰者模式是为了扩展对象,在不影响原功能的情况下增加更多的功能,且是一个可以叠加效果的(嵌套)。

分类: JavaScript设计模式与开发实践 标签: JavaScript模式装饰者模式

评论

暂无评论数据

暂无评论数据

目录