单体模式是javascript中最基本但又最有用的模式之一,它可能比其他任何模式都更常用。这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变更进行访问。通过确保单体对象只存在一份实例,你就可以确信自己的所有代码使用的都是全局资源。
这种模式在javascript中非常重要,也许比在其他任何语言中都更重要。在网页上使用全局变量有很大的风险,而用单体对象创建的命名空间则是清除这些全局变量的最佳手段之一。仅此一个原因你就该掌握这种模式,更别说它还有许多别的用途。
最简单的单体实际上就是一个对象字面量,它把一批有一定关联的方法和属性组织在一起:
//Basic Singleton.
var Singleton = {
attribute1:true,
attribute2:10,
method1:function(){},
method2:function(){}
}
在这个示例中,所有那些成员现在都可以通过变量Singleton来访问。为此可以使用圆点运算符:
Singleton.attribute1= false;
var total =Singleton.attribute2+5;
var result =Singleton.method1();
这个单体对象可以被修改。你可以为其添加新成员,这一点与别的对象字面量没什么不同。你也可以用delete运算符删除其现有成员。这实际上违背了面向对象的设计的一条原则:类可以被扩展,但不应该被修改。javascript中的所有对象都是易变的,这正是它与C++和Java等虽的面向对象语言的区别之一。你不必为此忧心忡忡(Python、Ruby和Smalltalk都允许在定义类之后又对其进行修改),但是你应该清楚在这种语言中无法阻止对象的修改。如果某些变量需要保护,那么你可以像前面演示的那样将其定义在闭包中。
你可能还没发觉这种单体对象与普通对象字面量有什么不同。按传统定义,单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。要是严格地按这个定义来说,前面的例子所示的并不是一个单体,因为它不是一个可实例化的类。我们打算把单体模式定义得更广义一些:单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
对象字面量只是用以创建单体的方法之一。
单体对象由两个部分组成:包含着方法和属性成员的对象自身,以及用来访问它的变量。这个变量通常是全局性的,以便在网页上任何地方都能直接访问到它所指向的单体对象。这个单体对象的所有成员都被包装在这个对象中,所以它们不是全局性的。由于这些成员只能通过这个单体对象变量进行访问,因此在某种意义上,可以说它们被单体对象圈在了一个命名空间中。
命名空间是可靠的javascript编程的一个重要工具。在javascript中什么都可以被改写,程序员一不留神就会擦除一个变量、函数甚至整个类,而自己却毫无察觉。这种错误找起来非常费时:
//declaredglobally
function findProduct(id){
…
}
//Later in yourpage, another programmer adds …
var resetProduct =$(‘reset-product-button’);
var findProduct =$(‘find-product-button’);//The findProduct function just got overwritten.
为了避免无意中改写变量,最好的解决办法之一是用单体对象将代码组织在命名空间之中。下面是前面的例子用单体模式改良后的结果
var myNamespace ={
findProduct:function(id){
…
},
// other methods can go here as well.
}
// Later in yourpage, another programmer adds …
var resetProduct =$(“reset-product-button”);
var findProduct =$(“find-product-button”);
现在findProduct函数是MyNamespace中的一个方法,它不会被全局命名空间中声明的任何新变更改写。要注意,该方法仍然可以从各个地方访问。不同之处在于现在其调用方式不是findProduct(id),而是MyNamespace.findProduct(id)。这还有一个好处就是,这可以让其他程序员大体知道这个方法的声明地点及作用。用命名空间把类似的方法组织到一起,也有助于增强代码的文档性。
命名空间还可以进一步分割。现在网页上的javascript代码往往不止有一个来源。其中除了你写的代码外,还会有库代码、广告代码和徽章代码。这些变量都出现在全局命名空间中。为了避免冲突,可以定义一个用来包含自己的所有代码的全局对象:
/*GiantCropnamespace.*/
var GiantCorp ={};
然后可以分门别类地把自己的代码和数据组织到这个全局对象中的各个对象(单体)中:
GiantCrop.Common={
//A singleton with common methodsused by all objects and modules.
}
GiantCrop.ErrorCodes={
//An object literal used to storedata.
}
GiantCrop.Common={
//A singleton with page specificmethods and attributes.
}
来源外部的代码与GiantCorp这个变量发生冲突的可能性很小。如果真有冲突,其造成的问题会非常明显,所以很容易发现。想到自己办事牢靠,没有把全局命名空间搞得一片狼藉,你大可高枕无忧。你只是在全局命名空间中加入了一个变量,这是一个javascript程序员可望获得的最小地盘。
我们已经了解了如何把单体作为命名空间使用,现在我们再介绍单体模式的一个特殊用途。在那处拥有许多网页的网站中,有些javascript代码是所有网页都要用到的,它们通常被存入在独立的文件中;而有些代码则是某个网页专用的,不会被用到其他地方。最好把这两种代码分别包装在自己的单体对象中。
用来包装种个网页专用的代码的单体通常看起来都差不多。它需要封装一些数据、为各网页特有的行为定义一些方法以及定义初始化方法。涉及DOM中特有元素的大多数代码,比如添加事件监听器的代码。
下面是用来包装特定网页专用代码的单体的骨架:
Namespace.PageName= {
CONSTANT_1:true,
CONSTANT_2:10,
//page methods
mehtod1:function(){},
method2:function(){},
//initialization method
init:function(){}
}
// invoke theinitialization method after the page loads.
我们用一个web开发中很常见的任务为例示范一下它的用法。我们经常想要用javascript为表单添加功能。出于平稳退化方面的考虑,通常先创建一个不依赖于javascript的、使用普通提交制完成任务的纯html网页。然后再用javascript控制表单行为,以提供额外的特性。
下面的单体会查找并劫持一个特定表单:
GiantCorp.RegPage= {
FORM_ID:”reg-form”,
OUTPUT_ID:”reg-results”,
//form handling methods.
handleSubmit:function(e){
e.preventDefault();//stop the normal form submissin.
var data ={};
var inputs =GiantCorp.RegRage.formEl.getElementsByTagName(“input”);
//collect the values of the input fields in the form.
for(vari=0,len=inputs.length;i data[inputs[i].name] = input[i].value; } //send theform values back to the server. GiantCrop.RegPage.sendRegistration(data); }, sendRegistration:function(data){ //make an xhr request and call displayResult() when response is received. }, displayResult:function(response){ //Output theresponse directly into the output element. We are assuming the server will sendback formatted HTML. GiantCorp.RegPage.outputEl.innerHTML = response; }, //Initialization method. init:function(){ //Get the form and output elements. GiantCorp.RegPage.formEl = $(GiantCorp.RegPage.FORM_ID); GiantCorp.RegPage.outputEl =$(GiantCorp.RegPage.OUTPUT_ID); //Hijack the form submission. addLoadEvent(GiantCorp.RegPage.init); } } 上述代码中首先假定GiantCorp命名空间已经作为一个空对象字面量被创建好啦。如若不然,代码第一行就会引发一个错误。下面这行代码可以防止这种错误,如果GiantCorp还不存在,它就会定义这个对象,其中使用的逻辑“或”运算符可以在未找到一个属性时为其提供一个默认值 var GiantCorp =window.GiantCorp || {}; 我们在前面的章节中讨论过几种创建类的私用成员的做法,使用真正私用方法的一个缺点在于它们比较耗费内存,因为每个实例都具有方法的一份新副本。不过由于单体对象只会被实例化一次,因此为其定义真正的私用方法时必顾虑内存方法的问题。 在单体对象内创建私用成员最简单、最直接的办法就是用下划线表示法,这可以让其他程序员知道相关方法或属性是私用的,只在对象内部使用。在单体对象中使用下划线表示法是一种告诫其他程序员不要直接访问特定成员的简明办法 如下: //DataParsersingleton,converts characher delimited strings into arrays. GiantCorp.DataParser= { //private methods. _stripWhitespace:function(str){ return str.replace(/\s/,’’); }, _stringSplit:function(str,delimiter){ returnstr.split(delimiter); }, //public method. stringToArray:function(str,delimiter,stripWS){ if(stripWS){ str =this._stripWhitespace(str); } var outputArray =this._stringSplit(str,delimiter); return outputArray; } } 在单体对象中创建私用成员的第二种办法需要借助闭包。这与前面提到的真正的私用成员的做法非常相似,但也一个重要区别。先前的做法是把变量和函数定义在构造函数体内(不使用this关键字)以使其成为私用成员,此外还在构造函数体内定义了所有的特权方法并用this关键字使其可被外界访问,每生成一个该类的实例时,所有声明在构造函数内的方法和属性都会再次创建一份。这可能会非常低效。 因为单体只会被实例化一次,所以你不用担心自己在构造函数中声明了多少成员。每个方法和属性都只会被创建一次,所以你可以把它们都声明在构造函数内部。 //Singleton as anObject Literal. MyNamespace.Singleton= {}; 现在我们用一个在定义之后立即执行的函数创建单体: //Singleton withprivate members,step 1 MyNamespace.Singleton= (function(){ return {}; })(); 注意第二个例子中并没有把一个函数给MyNamespace.Singleton。那个匿名函数返回一个对象,而赋给MyNamespace.Singleton变量的正是这个对象。 第二个例子的目的是它可以创建一个可以用来添加真正的私用成员闭包。任何声明在这个匿名函数中的变量或函数都只能被在同一个闭包中的声明的其他函数访问。这个闭包在匿名函数执行结束后依然存在,所以在其中声明的函数和变量总能从匿名函数所返回的对象内部访问。 //Singleton withprivate members step 3 MyNamespace.Singleton= (function(){ //private members. var privateAttribute1 =false; var privateAttribute2 =[1,2,3]; function privateMethod1(){…}, function private Method2(args){…} reutrn { //publicmembers. publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){…}, publicMethod2:function(){…} } })() 这种单体模式又称模块模式,指的是它可以把一批相关方法和属性组织为模块并直到划分命名空间的作用。 现在回到DataParser这个例子中来,看看如何在其实现中使用真正的私用成员。现在我们不再为每个私用方法名称的开关添加一个下划线,而是把这些方法定义在闭包中: //DataParsersingleton,converts character delimited strings into arrays. //Now using trueprivate methods. GiantCrop.DataParse= (function(){ //pirvate attribute var whitespaceRegex = /\s+/; //private methods function stripWhitespace(str){ return str.replace(whitespaceRegex,’’); } function stringSplit(str,delimiter){ return str.split(delimiter); } //Everything returened in the object literal is public,but can access the members in the closuer created above. stringToArray:function(str,delimiter,stripWS){ if(stripWS){ str =stripWhitespace(str); } var outputArray = stringSplit(str,delimiter); return outputArray; } })() 现在这些私用方法和属性可能直接用其名称访问,不必在其前面加上this或GiantCrop.DataParser.,这些前缀只用于访问于单体对象的公用成员。 这种模式与使用下划线表示法模式相比 有几点优势。把私用成员放到闭包中可以确保其不会在单体对象之外被使用。你可以自由地改变对象的实现细节,这不会殃及别人的代码。还可以用这种方法对数据进行保护和封装。 在使用这种模式时,你可以享受到真正的私用成员带来的所有好处,而不必付出什么代价,这是因为单体类只会被实例化一次。单体模式之所以是javascript最流行、应用最广泛的模式之一,原因即在于此。 前面所讲的单体模式各种实现方式有一个共同点:单体对象都是在脚本加载时被创建出来。对于资源密集型或配置开销甚大的单体,也许便合理的做法是将其实例化推迟到需要使用它的时候。这种技术被称为惰性加载,它最常用于那些必须加载大量数据的单体。而那些被作用的命名空间、特定网页专用代码包装器或组织相关实用方法的工具的单体最好还是立即实例化。 这种惰性加载单体的特别之处在于,对它们的访问必须借助于一个静态方法。应该这样调用其方法:Singleton.getInstance().methodName(), 而不是这样用调用:Singleton.methodName()。getInstance方法会检查该单体是否已经被实例化。如果还没有,那么它将创建并返回其实例。如果单体已经实例化过,那么它将返回现有的实例。下面我们从前面那个拥有真正的私用成员的单体的基本框架出发示范一下如何把普通单体转化为惰性加载单体: //Singleton withprivate members, MyNamespace.Singleton=(function(){ //private members var privateAttribute1 = false; var privateAttribute2 =[1,2,3]; function privateMethod1(){ … } function privateMethod2(){ … } return { //publicmembers. publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ } publicMehtod2:function(args){ } } })() 这段代码还没有进行任何修改,转化工作的第一步是把单体的所有代码移到一个名为constructor方法中: /*General skeletonfor a lazy loading singleton, step 1*/ MyNamespace.Singleton= (function(){ //all of the normal singleton code goeshere function constructor(){ //privatemembers varprivateAttribute1 = false; varprivateAttribute2 =[1,2,3]; functionprivateMethod1(){ … } functionprivateMethod2(){ … } } return { //publicmembers. publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ } publicMehtod2:function(args){ } } })() 这个方法不能从闭包外部访问,这是件好事,因为我们想全权控制其调用时机。公用方法getInstance就是用来实现这种控制的。为了 /*General skeletonfor a lazy loading singleton, step 2*/ MyNamespace.Singleton= (function(){ //all of the normal singleton code goeshere function constructor(){ //privatemembers varprivateAttribute1 = false; varprivateAttribute2 =[1,2,3]; functionprivateMethod1(){ … } functionprivateMethod2(){ … } return{ //public members. publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ } publicMehtod2:function(args){ } }; } return { getInstance:function(){ } })() 现在开始编写用于控制单体类实例化时机的代码。它需要做两件事。第一,它必须知道该类是否已经被实例化过。 第二,如果该类已经实例化过,那么它需要掌握其实例的情况,以便能返回这个实例。 /*General skeletonfor a lazy loading singleton, step 3*/ MyNamespace.Singleton= (function(){ //all of the normal singleton code goeshere function constructor(){ //privatemembers var uniqueInstance; //private attribute that holdsthe single instance. var privateAttribute1 = false; var privateAttribute2 =[1,2,3]; function privateMethod1(){ … } function privateMethod2(){ … } return{ //public members. publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ } publicMehtod2:function(args){ } }; } return { getInstance:function(){ if(!uniqueInstance){ uniqueInstance =constructor(); } return uniqueInstance; } })() 把一个单体转化为惰性加载单体后,你必须对调用它的代码进行修改。在本例中,像这样的方法调用: MyNamespace.Singleton.publicMethod1(); 应该转换为 MyNamespace.Singleton.getInstance().publicMethod1(); 分支:是一种用来把浏览器间的差异封装在运行期间进行设置的动态方法中的技术。如:我们需要创建一个返回XHR对象的方法。这种XHR对象在大多数浏览器中是XMLHttpRequest类的实例,而在IE早期版本则是某种ActiveX类的实例。这样一个方法通常会进行某种浏览器嗅探或对象探测。如果不用分支技术,那么每次调用这个方法时,所有那些流利器嗅探代码要再次运行。要是这个方法调用的很频繁,那么这样做会严重缺乏效率。 更有效的做法是只在脚本加载时一次性地确定针对特定浏览器的代码。这样一来,在初始化完成之后,每种浏览器都只会执行针对它的javascript实现而设计的代码。 我们可以创建两个不同的对象字面量,并根据某种条件将其中之一赋给那个变量 MyNamespace.Singleton= (function(){ var objA = { method1:function(){ }, method2:function(){ } }; var objB = { method1:function(){ }, method2:function(){ } } return (someCondition)?objA:objB; })(); 上述代码中创建了两个对象字面量,它们拥有相同的一套方法。对于使用这个单体的程序员来说,赋给MyNamespace.Singleton的究竟是哪个对象无关紧要,因为这两个对象实现了同样的接口,可以执行同样的任务,不同之处仅仅在于对象的方法具体使用的代码。分支技术不总是高效的选择。在前面的例子中,有两个对象被创建出来并保存在内存中,但派上用场的只有一个。在考虑是否使用这种技术的时候,你必须在缩短计算时间和占用更多内存这一利一弊之间权衡一下。 /*SimpleXhrFactorysingleton step 1. */ var SimpleXhrFactor= (function(){ var standard = { createXhrObject:function(){ return newXMLHttpRequest(); } } var activeXNew = { createXhrObject:function(){ return new ActiveXObject(“Msxml2.XMLHTTP”); } } var activeXOld = { createXhrObject:function(){ return new ActiveXObject(“Microsoft.XMLHTTP”); } } })(); 创建分支型单体的第2步是根据条件将3个分支中某一分支的对象赋给那个变量。其具体做法是逐一尝试每种XHR对象,直到遇到一个当前javascript环境所支持的对象为止:step 2. /*SimpleXhrFactorysingleton step 1. */ var SimpleXhrFactor= (function(){ var standard = { createXhrObject:function(){ return newXMLHttpRequest(); } } var activeXNew = { createXhrObject:function(){ return new ActiveXObject(“Msxml2.XMLHTTP”); } } var activeXOld = { createXhrObject:function(){ return new ActiveXObject(“Microsoft.XMLHTTP”); } } var testObject; try{ testObject = standard.createXhrObject(); return standard; }catch(e){ try{ testObject =activeXNew.createXhrObject(); return activeXNew; }catch(e){ try{ testObject =activeXOld.createXhrObject(); return activeXOld; }catch(e){ throw new Error(‘No XHRobject found in this environment. ’); } } } })();拥有私用成员的单体
使用下划线表示方法
使用闭包
两种技术的比较
惰性实例化
分支
用分支技术创建XHR对象