单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
注:单体是一个只能被实例化一次并且可以通过一个众所周知的访问点访问的类。——传统的定义
在单体对象中使用下划线表示法是一种告诫其他程序员不要直接访问特定成员的简明办法:
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;
}
}
简单的单体是这样的:
MyNamespace.Singleton = {};
现在,用一个立即执行函数创建单体:
MyNamespace.Singleton = function() {
return {};
}();
//或者这样:
MyNamespace.Singleton = (function() {
return {};
})();
下面代码示范在匿名函数中添加私有成员:
MyNamespace.Singleton = (function() {
//私有成员
var privateAttribute1 = false;
var privateAttribute2 = [1, 2, 3];
function privateMethod1() {
//...
}
function privateMethod2(args) {
//...
}
//公有成员
return {
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function() {
//...
},
publicMethod2: function(args) {
//...
}
}
})();
这种单体模式又称模块模式。它可以把一批相关方法和属性组织为模块并起到划分命名空间的作用。
对于1中的例子,用闭包实现一下:
GiantCorp.DataParser = (function() {
//私有属性
var whitespaceRegex = /\s+/;
//私有方法
function stripWhitespace(str) {
return str.replace(whitespaceRegex,'');
}
function stringSplit(str, delimiter) {
return str.split(delimiter);
},
return {
//公有方法
stringToArray: function(str, delimiter, stripWS) {
if(stripWS) {
str = stripWhitespace(str); //不需要this.来访问
}
var outputArray = stringSplit(str, delimiter); //同上
return outputArray;
}
}
})();
this.
或GiantCorp.DataParser.
前缀,前缀只用于访问单体对象的公用成员。对惰性加载的单体的访问必须借助于一个静态方法。应该这样调用其方法:Singleton.getInstance().methodName()
,而不是这样调用:Singleton.methodName()
。getInstance方法会检查该单体是否已经被实例化。如果没有,它将创建并返回其实例。如果单体已经实例化过,那么它将返回现有实例。
下面示范一下如何把普通单体转化为惰性加载单体:
MyNamespace.Singleton = (function() {
//私有成员
var privateAttribute1 = false;
var privateAttribute2 = [1, 2, 3];
function privateMethod1() {
//...
}
function privateMethod2(args) {
//...
}
//公有成员
return {
publicAttribute1: true,
publicAttribute2: 10,
publicMethod1: function() {
//...
},
publicMethod2: function(args) {
//...
}
}
})();
1、 转化工作的第一步是把单体的所有代码转移到一个名为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) {
//...
}
}
}
})();
2、constructor方法不能从闭包外部访问,我们可以全权控制其调用时机。公有方法getInstance就是用来实现这种控制的。为了让constructor方法成为公用方法,只需要将其放到一个对象字面量中并返回该对象即可:
MyNamespace.Singleton = (function() {
function constructor() {
...
}
return {
getInstance: function() {
//这里是控制代码
}
}
})();
3、现在开始编写用于控制单体类实例化时机的代码。它需要做两件事:
做这两件事需要用到一个私有属性和已有的私有方法constructor
:
MyNamespace.Singleton = (function() {
//私有属性
var uniqueInstance;
function constructor() {
...
}
return {
getInstance: function() {
//若没有被实例化过
if(!uniqueInstance) {
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
把一个单体转化为惰性加载单体后,调用方式需要修改。像这样的方法调用:
MyNamespace.Singleton.publicMethod1()
应该被改为这样的形式:
MyNamespace.Singleton.getInstance().publicMethod1()
var MNS = MyNamespace.Singleton
。这样会创建一个全局变量,最好还是把它声明在一个特定网页专用代码包装器单体中。在存在单体嵌套的情况下会出现一些作用域方面的问题。在这种场合下访问其他成员最好使用完全限定名(如GiantCorp.SingletonName)而不是this。分支是一种用来把浏览器之间的差异封装到在运行期间进行设置的动态方法中的技术。
例如,我们需要创建一个返回XHR对象的方法。这种XHR对象在大多数浏览器中是XMLHttpRequest类的实例,而在IE早期版本中则是某种ActiveX类的实例。这样一个方法通常会进行某种浏览器嗅探或对象探测。如果不使用分支技术,那么每次调用该方法,所有的那些浏览器嗅探代码都要再次运行。要是这个方法调用频繁,就会严重缺乏效率。
更有效的做法是只在脚本加载时一次性地确定针对特定浏览器的代码。这样,在初始化完成后,每种浏览器都只会执行针对它的JavaScript实现而设计的代码。
我们可以创建两个不同的字面量对象,并根据某种条件将其中之一赋给那个变量:
MyNamespace.Singleton = (function() {
var objectA = {
method1: function() {
...
},
method2: function() {
...
}
};
var objectB = {
method1: function() {
...
},
method2: function() {
...
}
};
return (someCondition) ? objectA : objectB;
})();
下面这个例子就属于适合采用分支技术的情况,因为其中分支对象较小而判断使用哪个对象的开销较大。
在本例中我们创建一个单体,它有一个生成XHR对象实例的方法。
var SimpleXhrFactory = (function() {
var standard = {
createXhrObject: function() {
return new XMLHttpRequest();
}
};
var activeXNew = {
createXhrObject: function() {
return new ActiveXObject('Msxml2.XMLHTTP');
}
};
var activeXOld = {
createXhrObject: function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
};
})();
这3个分支各包含一个对象字面量,都有一个名为createXhrObject的方法,这个方法做的是返回一个XHR对象。
- 第二步是根据条件将3个分支中某一分支的对象赋给那个变量。具体做法是逐一尝试每种XHR对象,直到遇到一个当前JavaScript环境支持的对象:
var SimpleXhrFactory = (function() {
var standard = {
createXhrObject: function() {
return new XMLHttpRequest();
}
};
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 XHR object found in this environment.');
}
}
}
})();
使用该API的程序员只要调用SimpleXhrFactory.createXhrObject()就能得到适合特定运行环境的XHR对象。用了分支技术后,所有的特性嗅探代码都只会执行一次,而不是每生成一个对象就要执行一次。
主要是对代码的组织作用。