JavaScript单体模式

单体模式是javascript中最基本但又最有用的模式之一,它可能比其他任何模式都更常用。这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变更进行访问。通过确保单体对象只存在一份实例,你就可以确信自己的所有代码使用的都是全局资源。

这种模式在javascript中非常重要,也许比在其他任何语言中都更重要。在网页上使用全局变量有很大的风险,而用单体对象创建的命名空间则是清除这些全局变量的最佳手段之一。仅此一个原因你就该掌握这种模式,更别说它还有许多别的用途。

单体的基本结构

最简单的单体实际上就是一个对象字面量,它把一批有一定关联的方法和属性组织在一起:

/* Basic Singleton. */

var Singleton = {
  attribute1: true,attribute2: 10function() {
  },method2: (arg) {
  }
};

Singleton.attribute1 = false;
var total = Singleton.attribute2 + 5var result = Singleton.method1();

在这个示例中,所有那些成员现在都可以通过变量Singleton来访问。为此可以使用圆点运算符:

Singleton.attribute1= false;

var total =Singleton.attribute2+5;

var result =Singleton.method1();

这个单体对象可以被修改。你可以为其添加新成员,这一点与别的对象字面量没什么不同。你也可以用delete运算符删除其现有成员。这实际上违背了面向对象的设计的一条原则:类可以被扩展,但不应该被修改

你可能还没发觉这种单体对象与普通对象字面量有什么不同。按传统定义,单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。要是严格地按这个定义来说,前面的例子所示的并不是一个单体,因为它不是一个可实例化的类。我们打算把单体模式定义得更广义一些:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。

对象字面量只是用以创建单体的方法之一。

划分命名空间

单体对象由两个部分组成:包含着方法和属性成员的对象自身,以及用来访问它的变量。这个变量通常是全局性的,以便在网页上任何地方都能直接访问到它所指向的单体对象。这个单体对象的所有成员都被包装在这个对象中,所以它们不是全局性的。由于这些成员只能通过这个单体对象变量进行访问,因此在某种意义上,可以说它们被单体对象圈在了一个命名空间中。

命名空间是可靠的javascript编程的一个重要工具。在javascript中什么都可以被改写,程序员一不留神就会擦除一个变量、函数甚至整个类,而自己却毫无察觉。这种错误找起来非常费时:

 Declared globally. */    

 findProduct(id) {
  ...
}

...

// Later in your page,another programmer adds...
var resetProduct = $('reset-product-button');
var findProduct = $('find-product-button');  The findProduct function just got
                                             overwritten.

为了避免无意中的改写变量,最好的解决办法之一就是用单体对象将代码组织在命名空间中。下面的例子就是用单体模式改良后的结果:

 Using a namespace. var MyNamespace = {
  findProduct: (id) {
      ...
  }, Other methods can go here as well.
}
...

 Nothing was overwritten.

现在findProduct函数是MyNamespace中的一个方法,它不会被全局命名空间中声明的任何新变更改写。要注意,该方法仍然可以从各个地方访问。不同之处在于现在其调用方式不是findProduct(id),而是MyNamespace.findProduct(id)。这还有一个好处就是,这可以让其他程序员大体知道这个方法的声明地点及作用。用命名空间把类似的方法组织到一起,也有助于增强代码的文档性。

但是,要指出的是,MyNamespace是一个糟糕的单体名字,命名空间应该能够说明其中的代码的用途,在本例中用ProductTools这个名字更为恰当。

命名空间还可以进一步分割。现在网页上的javascript代码往往不止有一个来源。其中除了你写的代码外,还会有库代码、广告代码和徽章代码。这些变量都出现在全局命名空间中。为了避免冲突,可以定义一个用来包含自己的所有代码的全局对象:

* GiantCorp namespace. */
var GiantCorp = {};

然后可以分门别类地把自己的代码和数据组织到这个全局对象中的各个对象(单体)中:

GiantCorp.Common = {
   A singleton with common methods used by all objects and modules.
};

GiantCorp.ErrorCodes = An object literal used to store data.
};

GiantCorp.PageHandler = A singleton with page specific methods and attributes.
};

来源外部的代码与GiantCorp这个变量发生冲突的可能性很小。如果真有冲突,其造成的问题会非常明显,所以很容易发现。想到自己办事牢靠,没有把全局命名空间搞得一片狼藉,你大可高枕无忧。你只是在全局命名空间中加入了一个变量,这是一个javascript程序员可望获得的最小地盘。

 

拥有私用成员的单体

我们在前面的章节中讨论过几种创建类的私用成员的做法,使用真正私用方法的一个缺点在于它们比较耗费内存,因为每个实例都具有方法的一份新副本。不过由于单体对象只会被实例化一次,因此为其定义真正的私用方法时必顾虑内存方法的问题。

使用下划线表示方法

在单体对象内创建私用成员最简单、最直接的办法就是用下划线表示法,这可以让其他程序员知道相关方法或属性是私用的,只在对象内部使用。在单体对象中使用下划线表示法是一种告诫其他程序员不要直接访问特定成员的简明办法  如下:

 DataParser singleton,converts character delimited strings into arrays. */ 

GiantCorp.DataParser = Private methods.
  _stripWhitespace: (str) {
    return str.replace(/\s+/,'');
  },_stringSplit: (str,delimiter) {
    return str.split(delimiter);
  },1)"> Public method.
  stringToArray: if(stripWS) {
      str = this._stripWhitespace(str);
    }
    var outputArray = ._stringSplit(str,delimiter);
     outputArray;
  }
};

使用闭包

在单体对象中创建私用成员的第二种办法需要借助闭包。这与js闭包应用文章里提到的真正的私用成员的做法非常相似,但也一个重要区别。先前的做法是把变量和函数定义在构造函数体内(不使用this关键字)以使其成为私用成员,此外还在构造函数体内定义了所有的特权方法并用this关键字使其可被外界访问,每生成一个该类的实例时,所有声明在构造函数内的方法和属性都会再次创建一份。这可能会非常低效。

因为单体只会被实例化一次,所以你不用担心自己在构造函数中声明了多少成员。每个方法和属性都只会被创建一次,所以你可以把它们都声明在构造函数内部。

 Singleton as an Object Literal. 
MyNamespace.Singleton = {};

现在我们用一个在定义之后立即执行的函数创建单体:

 Singleton with Private Members,step 1. 
MyNamespace.Singleton = (() {
   {};
})();

对上面的在进行一次改进,添加私有成员以及返回公共函数。任何声明在这个匿名函数中的变量或函数都只能被在同一个闭包中的声明的其他函数访问。这个闭包在匿名函数执行结束后依然存在,所以在其中声明的函数和变量总能从匿名函数所返回的对象内部访问。



MyNamespace.Singleton = ( Private members.
  var privateAttribute1 = ;
  var privateAttribute2 = [1,2,3];
  
   privateMethod1() {
    ...
  }
   privateMethod2(args) {
    ...
  }

  return {  Public members.
    publicAttribute1: () {
      ...
    },publicMethod2: (args) {
      ...
    }
  };
})();

这种单体模式又称模块模式,指的是它可以把一批相关方法和属性组织为模块并直到划分命名空间的作用。

两种技术的比较

现在回到DataParser这个例子中来,看看如何在其实现中使用真正的私用成员。现在我们不再为每个私用方法名称的开关添加一个下划线,而是把这些方法定义在闭包中:

*/ 
   Now using true private methods. 

GiantCorp.DataParser = ( Private attributes.
  var whitespaceRegex = /\s+/;
  
   Private methods.
   stripWhitespace(str) {
    return str.replace(whitespaceRegex,1)">);
  }
   stringSplit(str,1)"> str.split(delimiter);
  }
  
   Everything returned in the object literal is public,but can access the
   members in the closure created above.
   { 
     Public method.
    stringToArray: (stripWS) {
        str = stripWhitespace(str);
      }
      var outputArray = outputArray;
    }
  };
})();  Invoke the function and assign the returned object literal to 
      GiantCorp.DataParser.

现在这些私用方法和属性可能直接用其名称访问,不必在其前面加上this或GiantCrop.DataParser.,这些前缀只用于访问于单体对象的公用成员。

这种模式与使用下划线表示法模式相比 有几点优势。把私用成员放到闭包中可以确保其不会在单体对象之外被使用。你可以自由地改变对象的实现细节,这不会殃及别人的代码。还可以用这种方法对数据进行保护和封装。

在使用这种模式时,你可以享受到真正的私用成员带来的所有好处,而不必付出什么代价,这是因为单体类只会被实例化一次。单体模式之所以是javascript最流行、应用最广泛的模式之一,原因即在于此。

 

惰性实例化

前面所讲的单体模式各种实现方式有一个共同点:单体对象都是在脚本加载时被创建出来。对于资源密集型或配置开销甚大的单体,也许便合理的做法是将其实例化推迟到需要使用它的时候。这种技术被称为惰性加载,它最常用于那些必须加载大量数据的单体。而那些被作用的命名空间、特定网页专用代码包装器或组织相关实用方法的工具的单体最好还是立即实例化。

这种惰性加载单体的特别之处在于,对它们的访问必须借助于一个静态方法。应该这样调用其方法:Singleton.getInstance().methodName(),而不是这样用调用:Singleton.methodName()。getInstance方法会检查该单体是否已经被实例化。如果还没有,那么它将创建并返回其实例。如果单体已经实例化过,那么它将返回现有的实例。下面我们从前面那个拥有真正的私用成员的单体的基本框架出发示范一下如何把普通单体转化为惰性加载单体:

 General skeleton for a lazy loading singleton,1)">() {

  function constructor() {  All of the normal singleton code goes here.
     Private members.
    ;
    ];
  
     privateMethod1() {
      ...
    }
     privateMethod2(args) {
      ...
    }

     Public members.
      publicAttribute1: () {
        ...
      },1)">(args) {
        ...
      }
    }
  }
  
})();

() {
  
   All of the normal singleton code goes here.
    ...
  }
  
   {
    getInstance: () {
       Control code goes here.
    }
  }
})();

var uniqueInstance;  Private attribute that holds the single instance.
  
  if(!uniqueInstance) {  Instantiate only if the instance doesn't exist.
        uniqueInstance = constructor();
      }
       uniqueInstance;
    }
  }
})();

以上使我们对模块模式的修改。转化工作的第一步是把单体的所有代码移到一个名为constructor方法中。

把一个单体转化为惰性加载单体后,你必须对调用它的代码进行修改。在本例中,像这样的方法调用:

MyNamespace.Singleton.publicMethod1();

应该转换为

MyNamespace.Singleton.getInstance().publicMethod1();

 
GiantCorp.DataParser = { 
  Private methods.  
_stripWhitespace: (str) {    
);  
},delimiter) {    
 str.split(delimiter);  },1)"> Public method.  
stringToArray: (stripWS) {     
 str = ._stripWhitespace(str);    
}    
 outputArray;  
}
};

 

 

分支

分支:是一种用来把浏览器间的差异封装在运行期间进行设置的动态方法中的技术。如:我们需要创建一个返回XHR对象的方法。这种XHR对象在大多数浏览器中是XMLHttpRequest类的实例,而在IE早期版本则是某种ActiveX类的实例。这样一个方法通常会进行某种浏览器嗅探或对象探测。如果不用分支技术,那么每次调用这个方法时,所有那些流利器嗅探代码要再次运行。要是这个方法调用的很频繁,那么这样做会严重缺乏效率。

更有效的做法是只在脚本加载时一次性地确定针对特定浏览器的代码。这样一来,在初始化完成之后,每种浏览器都只会执行针对它的javascript实现而设计的代码。

我们可以创建两个不同的对象字面量,并根据某种条件将其中之一赋给那个变量

MyNamespace.Singleton= ((){

     var objA = {
                method1:(){
                },method2:(){
               }
          };

     var objB =(){
               }
          }
         return (someCondition)?objA:objB;
})();

 

上述代码中创建了两个对象字面量,它们拥有相同的一套方法。对于使用这个单体的程序员来说,赋给MyNamespace.Singleton的究竟是哪个对象无关紧要,因为这两个对象实现了同样的接口,可以执行同样的任务,不同之处仅仅在于对象的方法具体使用的代码。分支技术不总是高效的选择。在前面的例子中,有两个对象被创建出来并保存在内存中,但派上用场的只有一个。在考虑是否使用这种技术的时候,你必须在缩短计算时间和占用更多内存这一利一弊之间权衡一下。

用分支技术创建XHR对象

SimpleXhrFactorysingleton step 1. var SimpleXhrFactor= ((){
     var standard = {
               createXhrObject:(){
                         newXMLHttpRequest();
               }
           }

     var activeXNew =return new ActiveXObject(“Msxml2.XMLHTTP”);
               }
           }

     var activeXOld = ActiveXObject(“Microsoft.XMLHTTP”);
               }
           }
})();

 

创建分支型单体的第2步是根据条件将3个分支中某一分支的对象赋给那个变量。其具体做法是逐一尝试每种XHR对象,直到遇到一个当前javascript环境所支持的对象为止:step 2.

 {

               createXhrObject:(){

                         newXMLHttpRequest();

               }

           }

      ActiveXObject(“Msxml2.XMLHTTP”);

               }

           }

      ActiveXObject(“Microsoft.XMLHTTP”);

               }

           }

     var testObject;

      try{

            testObject = standard.createXhrObject();

            standard;

      }catch(e){

          {

                testObject =activeXNew.createXhrObject();

                activeXNew;

          }(e){

                   {

                      testObject =activeXOld.createXhrObject();

                       activeXOld;

                  }(e){

                      throw new Error(‘No XHRobject found in  environment. ’);

                 }

          }

     }

})();

 
View Code

 

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结