装饰者模式

装饰者模式的作用是动态地给某个对象增加一些方法,javascript是一个没有类的世界,通常,在一种语言中,类用来制作对象(就像用一个模板来制作蛋糕一样,蛋糕是一个对象,具有体积,可以被吃,而模板本身则并没有意义)javascrpt则不同,在javascript的世界中,没有模板,我们有的是一个原始的对象,所有的对象都克隆自这个对象(或相互克隆)并新增了自己的一些属性和方法来完成不同的职责,这个根对象就是Object.prototype。

因此,如果需要对javascript中对象进行修改,大刀阔斧去改即可,不必涉及到类的概念,所以javascript中装饰者的实现相较于java等语言要方便很多。

有的读者可能会问,既然修改一个对象如此简单,那么我为什么还需要引入装饰者模式?下面,我们就用统计需求这个案例来对装饰者模式的必要性进行一些探讨。

需求描述

在你的新页面中,有三个可点击按钮,点击时,其中两个按钮会执行相同的函数,另一个按钮执行不同的函数。页面的基本信息已经给出。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <button id="btn1">按钮1</button>
    <button id="btn2">按钮2</button>
    <button id="btn3">按钮3</button>
    <script>
    window.onload = function(){
        function funA (){
            console.log("函数A,点击按钮1、2时执行")
        }
        function funB (){
            console.log("函数B,点击按钮3时执行")
        }
        //为三个按钮绑定点击事件
        document.querySelector("#btn1").addEventListener("click",funA);
        document.querySelector("#btn2").addEventListener("click",funA);
        document.querySelector("#btn3").addEventListener("click",funB);
    }   
    </script>
</body>
</html>

现在你需要在页面中加入统计需求,即用户每点击一个按钮,都要向服务器someServer.com发送一个动态http请求,通过GET方式告诉服务器所执行函数的名称和当前的时间。

为什么需要装饰者

我想,你肯定很快就对这个需求有了自己的解决方案:先写一个通用函数来专门发送请求,然后更改funA和funB,在每个函数末尾调用刚刚写的通用函数。没问题,你说干就干。

//通知服务器的函数
function informServer(btnName){
    //发起http请求
    (new Image).src="http://someSever.com?name=" + btnName + "&time" + new Date().getTime();
}
function funA (){
    console.log("函数A,点击按钮1、2时执行");
    informServer("btn1");
}
function funB (){
    console.log("函数B,点击按钮3时执行");
    informServer("btn1");
}

但是改着改着,你有点不自在,这种对已有函数进行修改的行为总是让人不自在。的确,我们希望程序是低耦合的,每个函数都有自己独特的功能而且不对其他对象造成影响,而函数内部对调用者来说,应该是透明的。频繁的更改函数内部会破坏函数的封装性。这就是我们需要装饰者的原因。我们需要一种真正“动态”,低耦合的解决方案。

使用装饰者模式

在正式接触装饰着模式之前,我们需要复习一些javascript的一些基础知识。

刚刚我们讲过,在javascript的世界中,对象之间虽然是克隆而非继承关系,但是也能实现被克隆者的全部功能,因此对象与对象之间的克隆和对象于类之间的继承有异曲同工之妙。如果对象B克隆自对象A,那么A就叫做B的原型。同时如果A克隆自Object.prototype,那么Object.prototype也是A的原型,在Object.prototype、A、B之间,就形成了一种链式的关系,这条链就叫做原型链。当对象的某个方法(或属性)被调用的时候,如果他不能找到这个方法,则会沿着原型链向上寻找,直到找到这个方法为止。

在JavaScript中,函数是第一等对象,不仅因为它既可以像普通对象一样拥有属性和方法,而且它可以被调用。所以函数也遵循原型链的规则,一般来说,我们创建的函数(如上文的funA)的原型均为Function.prototype,Function.prototype的原型为Object.prototype。所以,每当我们创建一个函数的时候,都会形成这样一条原型链:

刚刚我们说道,函数也是对象,因此函数也有方法,常见的call、apply等方法就是写在Function.prototype中的。

请观察下列代码

//通过原型链进行装饰
//为Function.prototype增加方法以便所有函数都可以调用
Function.prototype.after = function(fn){
    //保留当前的上下文
    var that = this;
    //定义一个新的函数
    var fun = function(){
        //执行原函数
        that.apply(this,arguments);
        //执行被传入的函数
        fn.apply(this, arguments);
    }
    //返回的函数中将有两个步骤,第一个步骤是执行原函数,第二个步骤是执行传入的函数
    return fun;
}

我们在上述代码中扩展了Function.prototype,为之增加了after方法,这使得所有的函数都能通过调用after方法来定义本函数执行之后执行怎样的操作,应用到本例中,全部的代码应该是这样的。

window.onload = function(){
    function funA (){
        console.log("函数A,点击按钮1、2时执行")
    }
    function funB (){
        console.log("函数B,点击按钮3时执行")
    }

    //通知服务器的函数
    function informServer(btnName){
        //发起http请求
        (new Image).src="http://someSever.com?name=" + btnName + "&time" + new Date().getTime();
    }
    //通过原型链进行装饰
    Function.prototype.after = function(fn){
        var that = this;
        var fun = function(){
            that.apply(this,arguments);
            fn.apply(this, arguments);
        }
        return fun;
    }
    //调用原型链装饰
    funA = funA.after(function(){
        informServer("funA");
    });
    funB = funB.after(function(){
        informServer("funB");
    });

    //为三个按钮绑定点击事件
    document.querySelector("#btn1").addEventListener("click",funA);
    document.querySelector("#btn2").addEventListener("click",funA);
    document.querySelector("#btn3").addEventListener("click",funB);
}

funA和funB原本是两个具有不同功能的函数,而统计需求又是一个独立的功能,将不同的功能耦合在同一个函数中,会让函数变得庞大和混乱,通过这个demo的实践,我们使用装饰者模式,将统计功能作为额外职责动态的添加到原本的函数中,如果某天我们需要更改或删除统计需求,那么我们就可以非常精确和灵活的完成任务。

当然,如果你不喜欢更改Function.prototype,认为这是一种污染原型的做法,则也可以用普通的函数来实现这个demo。这部分读者可以自行实践。