单体模式
单体模式是javascript中最基本但最有用的模式之一。
这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问。通过确保单体对象只存在一个实例。
单体的基本结构
最简单的单体实际上就是一个字面量。它把一批有关联的方法和属性组织在一起:
/* Basic Singleton */
var Singleton = {
attribute1 : true,
attribute2 : 10,
method1 : function(){
},
method2 : function(){
}
};
所有那些成员现在可以通过变量Singleton来访问。
严格意义上说这个例子不是一个单例,因为它不是一个可实例化的的类。
我们把 单体 定义为:
单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象。它可以被实例化,那么它只能被实例化一次。
划分命名空间
单体对象由两部分组成:
- 包含着方法和属性成员的对象本身
- 以及用于访问它的变量
这个变量是全局的,保证在网页的任何部分可以直接访问到它所指的单体。
/* 声明全局变量 */
function findProduct(id){
...
}
...
// 之后在你的页面中,其他程序员写成这样..
var resetProduct = $("reset-product-button");
var findProduct = $("find-product-button"); // 这个 findProject方法被重写
为了避免无意改写变量,最好的解决办法之一就是用单体对象将代码组织在命名空间中,下面就式改良过的结果:
/* 使用命名空间 */
var MyNamespace = {
findProject : function(id){
...
},
// 其他的方法也可以写在这
}
...
// 之后的页面,其他的程序员写成这样..
var resetProduct = $("reset-product-button");
var findProduct = $("find-product-button"); // 这里没有函数被重写
现在 findProduct函数是MyNamespace中的一个方法,它不会被任何全局变量改写。并且这样可以让其他程序押金大体知道这个方法的声明地点及作用。也有助于增强代码的文档性。
命名空间还可以进一步分割。除了你写的代码,还会有库代码,广告代码,和徽章代码。这些变量都出现在网页的全局命名空间中,为了避免冲突,可以定义一个包含自己全部代码的全局变量:
/* GiantCorp namespace */
var GiantCorp = {};
// 然后把分门别类的把代码和数据组织到这个全局对象的各个对象(单体)中:
GiantCorp.Common = {
// 一个单体用于所有对象和模块的 common方法
};
GiantCorp.ErrorCodes = {
// 一个对象用于储存错误数据
};
GiantCorp.PageHandler = {
// 一个单体用于页面的具体方法和属性
};
用作特定网页专用代码的包装器的单体
用来包装各个网页专用的代码的单体通常差不多,需要封装一些数据,为各网页特有的行为定义一些方法以及定义初始化方法。
下面是用来包装特定网页专用代码的单体的骨架:
/* 一般的页面对象 */
Namespace.PageName = {
// 页面中的常量
CONSTANT_1 : true,
CONSTANT_2 : 10,
// 页面中的方法
method1 : function(){
},
method2 : function(){
},
// 初始化方法
init : function(){
}
};
// 在页面加载的时候调用这个init方法
addLoadEvent(Namespace.PageName.init);
下面用一个常见的任务为例示范一下它的用法,用JavaScript为表单添加功能。
/* 注册表单体,页面管理器对象 */
GiantCorp.RegPage = {
// Constants
FORM_ID : "reg-form",
OUTPUT_ID : "reg-result",
// 表单管理器方法
handleSubmit : function(e){
e.preventDefault();
var data = {};
var inputs = GiantCorp.RegPage.formEl.getElementsByTagName("input");
// 收集表单域的 value值
for(var i = 0, len = inputs.length; i < len; i++){
data[input[i].name] = inputs[i].value;
}
// 把 表单中的 values值传回服务器
GiantCorp.RegPage.sendRegistration(data);
},
sendRegistration : function(data){
// 创建一个 XHR请求,接收请求时调用 displayResult()方法
...
},
displayResult : function(response){
// 立即在 output element里输出请求, 我们将把服务器传回的数据改到HTML中
GiantCorp.RegPage.outputEl.innerHTML = response;
},
// 初始化方法
init : function(){
// 获取表单和 output element
GiantCorp.RegPage.formEl = $(GiantCorp.RegPage.FORM_ID);
GiantCorp.RegPage.outputEl = $("GiantCorp.RegPage.OUTPUT_ID");
// 劫持表单提交
addEvent(GiantCorp.RegPage.formEl, "submit", GiantCorp.RegPage.handlerSubmit);
}
};
// 页面载入时,调用这个函数
addLoadEvent(GiantCorp.RegPage.init);
上述代码嘉定GiantCorp命名空间已经作为一个空对象字面量创建好了,如果不是将报错,下面这段代码可以防止错误:
var GiantCorp = window.GiantCorp || {};
拥有私用成员的单体
使用真正的私用方法的一个缺点在于,比较消耗内存,因为每个实例都具有方法的一个新实例。不过单体模式只会被实例化一次,因此不用顾虑内存问题。创建伪私用方法,还是比较容易一些。
使用下划线表示法
在单体对象中创建私用成员,最简单最直接的方式是使用下划线表示法。这可以让其他程序员知道哪些方法和属性是私用的,只在对象内部用的。
/* 数据分析单体,把字符创分割成数组 */
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._stripWhitespace(str);
}
var outputArray = this._stringSplit(str, delimiter);
return outputArray;
}
};
使用闭包
在单体对象中创建私用成员的第二种方法需要借助闭包。
先前的做法是把变量和函数定义在构造函数体内(不使用this关键字)以使其成为私用成员,此外还在构造函数体内定义了所有特权方法并用this关键字使其被外界访问。每生成一个该类的实例,所有声明在构造函数内的方法和属性都会再创建一份,这样很低效。
因为单体模式只会被实例化一次,所以你不必担心在构造函数中声明了多少成员。没种方法和属性都只会被创建一次,所以可以把他们都声明在构造函数内部。
/* 单体作为一个对象字面量 */
MyNamespace.Singleton = {};
我们用一个在定义之后立即执行的函数创建单体:
/* 创建单体内的私用成员 第一步 */
MyNamespace.Singleton = function(){
return {};
}();
有点程序员喜欢在那个匿名函数之外再套上一对圆括号,以表示它会在声明之后立即执行。
MyNamespace.Singleton = (function(){
return {};
})();
可以想以前那样把公有成员添加到单体返回的那个对象字面量中:
/* 创建单体内私用成员 第二步 */
MyNamespace.Singleton = (function(){
return { // 公有成员
publicAttribute1 : true,
publicAttribute2 : 10,
publicMethod1 : function(){
...
},
publicMethod2 : function(args){
...
}
};
})();
要是这样得到的结果与直接使用一个对象字面量没什么区别,那又何必再加上外层函数包装呢?
原因在于这个包装函数创建了一个可以用来添加真正的私用成员闭包。任何声明在这个匿名函数中(但不是在那个对象字面量中)的变量只能被在同一个闭包中声明的其他函数访问。这个闭包在匿名函数执行结束后依然存在,所以在其中声明的函数和变量总能从匿名函数所返回的对象内部访问:
下面代码师范了匿名函数中添加私用成员的做法:
/* 创建单体私用成员 第三步 */
MyNamespace.Singleton = (function(){
// 私用成员
var privateAttribute1 = false;
var privateArrtibute2 = [1, 2, 3];
function privateMethod1(){
...
}
function privateMethod2(args){
...
}
return { // 公有成员
publicAttribute1 : true,
publicAttribute2 : 10,
publicMethods1 : function(){
...
},
publicMethod2 : function(args){
...
}
};
})();
这种单体模式又被称为模块模式,指的是它可以把一批相关的方法和属性组织为模块并起到划分命名空间的作用。
两种技术的比较
现在不再为每个私用方法名称开头添加下划线,而是把这些方法定义在闭包:
/* DataPerser 单体,把限制字数函数包含在数组中 */
?* 现在使用真正的私有方法 */
GiantCorp.DataParser = (function(){
// 私有属性
var whitespaceRegex = /\s+\;
// 私有方法
function stripWhitespace(str){
return str.replace(whitespaceRegex, "");
}
function stringSplit(str, delimiter){
return str.split(delimiter);
}
// 所有在对象字面量中返回的方法都是公开的,但是他们能访问闭包内成员
return {
// Public method
stringToArray : function(str, delimiter, stripWS){
if(stripWS){
str = stripWihtespace(str);
}
var outputArray = stringSplit(str, delimiter);
return outputArray;
}
}
})();
现在这些私用方法和属性可以直接用其名称访问,不必再其前面加上“this”或者“GiantCorp.DataParser”。
私有属性必须用 var声明,私有方法 按 function funcName(args) { … }
公有属性和方法分别按 attrName : attributeValue和 methodName : function(args) { … } 的形式。
惰性实例化
单体模式的各种实现方式有一个共同点:单体对象都是在脚本加载时创建出来。对于资源密集型的或者配置开销大的单体,将实例化推迟到需要使用它的时候。这被称为 惰性加载 。
惰性加载的特别之处,他们的访问必须借助一个静态方法。应该这样调用其方法 :Singleton.getInstance().methodName(),而不是调用:Singleton.methodName()。getInstance方法会检查该单体是否被实例化。如果没有,将创建并返回其实例。
下面将从前面那个拥有真正的私用成员单体框架出发转换成 惰性加载单体:
MyNamespace.Singleton = (function(){
// 私用成员
var privateAttribute1 = false;
var privateArrtibute2 = [1, 2, 3];
function privateMethod1(){
...
}
function privateMethod2(args){
...
}
return { // 公有成员
publicAttribute1 : true,
publicAttribute2 : 10,
publicMethods1 : function(){
...
},
publicMethod2 : function(args){
...
}
};
})();
转化的第一步是把单体所有代码转移到一个名为 constructor的方法中
/* 创建惰性加载单体基本框架 第一步 */
MyNamespace.Singleton = (function(){
function constructor(){ // 普通单体的代码都放这里
// 私有成员
var privateAttribute1 = false;
var privateAttribute2 = [1, 2, 3];
function privateMethod1(){
...
}
function privateMethod2(args){
...
}
return { // 公有成员
publicAttribute1 : true,
publicAttribute2 : 10,
publicMethod1 : function(){
...
},
publicMethod2 : function(args){
...
}
}
}
})();
这个方法不能从闭包外访问,这是件好事,因为想全权控制其调用时机,公有方法 getInstance就是用来实现这种控制的,为了使其成为公用方法,只需将其放到一个对象字面量中并返回该对象即可:
/* 创建惰性加载单体基本框架 第二步 */
MyNamespace.Singleton = (function(){
function constructor(){ // 单体普通代码放这里
...
}
return {
getInstance : function(){
// 控制代码放这里
}
}
})();
现在开始编写用于控制单体实例化时机的代码。需要两步:
- 必须知道该类是否已经被实例化
- 如果该类被实例化,那么它需要掌握其实例的情况,以便返回这个实例
/* 创建惰性加载单体基本框架 第三步 */
MyNamespace.Singleton = (function(){
var uniqueInstance; // 私有属性保证只有一个单体实例
function constructor(){ // 所有正常的单体代码放这里
...
}
return{
getInstance : function(){
if(!uniqueInstance){ // 只有单例实例不存在时创建单例
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
把一个单体转化为惰性加载单体后,你必须对它的代码进行修改。在本例中,像这样的方法调用:
MyNamepace.Singleton.publicMethod1();
应该改成这样:
MyNamespace.Singleton.getInstance().publicMethod1();
惰性加载单体的缺点就是其复杂性,用于创建单体的代码并不直观,而且不易理解。如果需要创建实例化单体,最好编写一条注释说明这样写的原因。
如果觉得命名空间过长,可以创建一个别名来简化。这种别名不过是保存了对特定对象的引用变量。可以把MyNamespace.Singleton简化为 MNS:
var MNS = MyNamespace.Singleton;
分支
分支是一种用来把浏览器差异封装到运行期间进行设置的动态方法中的技术。
我们创建两个不同的对象字面量,并根据某种条件将其中之一赋给那个变量:
/* 分支单体框架 */
MyNamespace.Singleton = (function(){
var objectA = {
method1 : function(){
...
},
method2 : function(){
...
}
};
var objectB = {
method1 : function(){
...
},
method2 : function(){
...
}
};
return (someCondition) > objectA : objectB;
})();
单体模式的适用场合
从为代码提供命名空间和增强其模块性这个角度,应该多使用单体模式。
简单的项目中,可以把单体用作命名空间,将代码组织在一个全局变量下。
复杂项目中,单体可以把相关代码组织在一起以便维护。
单体模式之利
单体模式的好处在于对代码的组织作用。把相关方法和属性组织在一个不会被多次实例化的单体中,可以让代码的调试和维护变得更轻松。描述性的命名空间还可以增强代码的自我说明性,有利于阅读。把方法包裹在单体中,可以防止他们被其他程序员误改。单体最好还是留给定义命名空间和实现分支型方法的这些用途。