js之装饰者模式

装饰者模式

在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。


level01:模拟传统面向对象语言的装饰者模式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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();
        // 分别输出: 发射普通子弹、发射导弹、发射原子弹

    </script>
</body>
</html>

导弹类和原子弹类的构造函数都接受参数 plane 对象,并且保存好这个参数,在它们的 fire方法中,除了执行自身的操作之外,还调用 plane 对象的 fire 方法。

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口( fire
方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的
下一个对象。

因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。


level02:回到 JavaScript 的装饰者

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        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();
        }
        //前者plane.fire()嵌套在后者plane.fire()里
        var fire2 = plane.fire;
        plane.fire = function(){
            fire2();
            atomDecorator();
        }
        plane.fire();
        // 分别输出: 发射普通子弹、发射导弹、发射原子弹
    </script>
</body>
</html>

level01:装饰函数

var a = function(){
    alert (1);
}
// 改成:
var a = function(){
    alert (1);
    alert (2);
}

缺点:直接违反了开放封闭原则。


level02:装饰函数

很多时候我们不想去碰原函数,也许原函数是由其他同事编的,里面的实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在一个我们不愿碰触的阴暗角落里。现在需要一个办法,在不改变函数源代码的情况下,能给函数增加功能,这正是开放-封闭原则给我们指出的光明道路。

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);
}

这样的代码当然是符合开放封闭原则的,我们在增加新功能的时候,确实没有修改原来的window.onload 代码,但是这种方式存在以下两个问题。

  1. 必须维护 _onload 这个中间变量,虽然看起来并不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量也会越来越多。
  2. 其实还遇到了 this 被劫持的问题,在 window.onload 的例子中没有这个烦恼,是因为调用
    普通函数 _onload 时, this 也指向 window ,跟调用 window.onload 时一样(函数作为对象的方法被调用时, this 指向该对象,所以此处 this 也只指向 window )。

在把 window.onload换成 document.getElementById ,代码如下:

var _getElementById = document.getElementById;
document.getElementById = function( id ){
    alert (1);
    return _getElementById( id ); // (1)
}
var button = document.getElementById( 'button' );

执行这段代码,我们看到在弹出 alert(1) 之后,紧接着控制台抛出了异常:// 输出: Uncaught TypeError: Illegal invocation

异常发生在(1) 处的 _getElementById( id ) 这句代码上,此时 _getElementById 是一个全局函数,当调用一个全局函数时, this 是指向 window 的,而 document.getElementById 方法的内部实现需要使用 this 引用, this 在这个方法内预期是指向 document ,而不是 window , 这是错误发生的原因,所以使用现在的方式给函数增加功能并不保险。

改进后的代码可以满足需求,我们要手动把 document 当作上下文 this 传入 _getElementById :

<html>
    <button id="button"></button>
<script>
    var _getElementById = document.getElementById;
    document.getElementById = function(){
        alert (1);
        return _getElementById.apply( document, arguments );
    }
    var button = document.getElementById( 'button' );
</script>
</html>

装饰者模式和代理模式

装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。

在虚拟代理实现图片预加载的例子中,本体负责设置 img 节点的 src,代理则提供了预加载的功能,这看起来也是“加入行为”的一种方式,但这种加入行为的方式和装饰者模式的偏重点
是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样,最终都是设置 src。但代理可以加入一些“聪明”的功能,比如在图片真正加载好之前,
先使用一张占位的 loading图片反馈给客户。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章