单体模式是JavaScript中最基本但又最有用的模式之一,它可能比其他任何模式都更常用。这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。通过确保单体对象只存在一份实例,你就可以确信自己的所有代码使用的都是同样的全局资源。
单体类在JavaScript中有许多用途。它们可以用来划分命名空间,以减少网页中全局变量的数目。它们还可以在一种名为“分支”的技术中用来封装浏览器之间的差异(借助分支技术,你在使用各种常用的工具函数时就不必再操心浏览器嗅探的事)。更重要的是,借助于单体模式,你可以把代码组织得更为一致,从而使其更容易阅读和维护。
单体的基本结构
最简单的单体实际就是一个对象字面量,它把一批有一定关联的方法和属性组织在一起:
var Singleton = { attribute1: true, attribute2: 10, method1: function() {}, method2: function() {} };
按传统的定义,单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。要是按照严格地定义来说,上述代码并不是一个单体,因为它不是一个可实例化的类。我们可以把单体模式定义得更广义一些:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
划分命名空间
单体对象由两个部分组成:包含着方法和属性成员的对象自身,以及用于访问它的变量。这个变量通常是全局性的。以便在网页上任何地方都能直接访问到它所指向的单体对象。因为单体对象的所有内部成员都被包装在这个对象中,所以它们不是全局性的。由于这些成员只能通过这个单体对象变量进行访问,因此在某种意义上,可以说它们被单体对象圈在了一个命名空间中。
拥有私有成员的单体
(1)使用下划线表示
在单体对象内创建私有成员最简单、最简截了当的方法是使用下划线表示法。
GiantCorp.DataParser = { _stripWhitespace: function(str){ return str.replace(/\s+/,''); }, _stringSplit: function(str, delimiter){ return str.split(delimiter); }, stringToArray: function(str, delimiter, stripWS) if(stripWS){ str = this._stringSplit(str, delimiter); } var outputArray = this._stringSplit(str, delimiter); return outputArray; } };
stringToArray方法中用this访问单体中的其他方法。这是访问单体中其他成员的最简便的做法。但这样做也有一点风险,因为this并不一定就指向GiantCorp.DataParser。例如,如果把某个方法用作事件监听器,那么其中的this可能会指向window对象,这意味着_stripWhitespace和_stringSplit这两个方法都不会被找到。虽然大多数JavaScript库都会为事件关联进行作用域校正,但还是使用全名GiantCorp.DataParser访问单体内的其他成员更保险一点。
(2)使用闭包
MyNamespace.Singleton = (function() {
return {
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function(){},
publicMethod2: function(){}
}
})()
这样得到的结果与直接使用一个对象字面量没什么区别,那又何必加上一层函数包装呢?原因在于这个包装函数创建了一个可以用来添加真正的私有函数的闭包。任何声明在这个匿名函数中(但不是在哪个对象字面量中)的变量或函数都只能被在同一个闭包中声明的其他函数访问。这个闭包在匿名函数执行结束后依然存在,所以在其中声明的函数和变量总能从匿名函数所返回的对象内部(并且也只能从内部)访问。
下面的代码示范了在匿名函数中添加私有成员的做法:
var MyNameSpace = {} MyNameSpace.Singleton = (function(){ // private members var privateAttribute1 = false; var privateAttribute2 = [1,2,3]; function privateMethod1(){ } function privateMethod2(args){ } return { // public members privateAttribute1: true, privateAttribute2: 10, privateMethod1: function(){}, privateMethod2: function(){} }; })();
这种单体模式又称模块模式,指的是它可以把一批相关方法和属性组织为模块并起到划分命名空间的作用。
1 var GiantCorp = {}; 2 GiantCorp.DataParser = (function(){ 3 // private attributes 4 var whitespaceRegex = /\s+/; 5 6 // private methods 7 function stripWhitespace(str){ 8 return str.replace(whitespaceRegex, ''); 9 } 10 function stringSplit(str, delimiter){ 11 return str.split(delimiter); 12 } 13 14 return { 15 // public method 16 stringToArray: function(str, delimiter, stripWS){ 17 if(stripWS){ 18 str = stripWhitespace(str); 19 } 20 var outputArray = stringSplit(str, delimiter); 21 return outputArray; 22 } 23 }; 24 })();
分支
分支是一种用来把浏览器间的差异封装到再运行期间进行设置的动态方法中的技术。举个栗子,假设我们需要创建一个返回XHR对象的方法。这种XHR对象在大多数浏览器中是XMLHttpRequest类的实例,而在IE早期版本中则是某种ActiveX类的实例。这样一个方法通常会进行某种浏览器嗅探或对象探测。如果不用分支技术,那么每次调用这个方法时,所有那些浏览器嗅探代码都要再次运行。要是这个方法的调用很频繁,那么这样做会严重缺乏效率。
更有效的做法是只在脚本加载时一次性地确定针对特定浏览器的代码。
var MyNamespace = {}; MyNamespace.Singleton = (function(){ var objectA = { method1: function(){}, method2: function(){} }; var objectB = { method1: function(){}, method2: function(){} }; return condition ? objectA : objectB; })();
分支技术并不总是更高效率的选择。在这个例子中,有两个对象(objectA和objectB)被创建出来并保存在内存中,但派上用场的只有一个。在考虑是否使用这种技术的时候,你必须在缩短计算时间和占用更多内存的利弊之间权衡一下。