第4章单例模式
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的window对象。在js开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录框,而这个浮窗是唯一的,无论单击多少次登录按钮,这个浮窗只会被创建一次。因此这个登录浮窗就适合用单例模式。
4.1 实现单便模式
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象:
var Singleton = function(name){
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function(){
alert(this.name);
}
Singleton.getInstance = function(name){
if(!this.instance){
this.instance = new Singleton(name);
}
return this.instance;
}
var a = Singleton.getInstance(‘sven1’);
var b = Singleton.getInstance(‘sven2’);
alert(a===b); //true;
或:
var Singleton = function(name){
this.name = name;
}
Singleton.prototype.getName = function(){
alert(this.name);
}
Singleton.getInstance = (function(){
var instance = null;
return function(name){
if(!instance){
instance = new Singleton(name);
}
return instance;
}
})();
我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,但有一个问题,就是增加了这个类的不透明性,Singleton类的使用者必须知道这是一个单例类,跟以往通过new XX的方式来获取对象不同,这里偏要使用Singleton.getInstance来获取对象。
var a = Singleton.getInstance(‘sven1’);
var b = Singleton.getInstance(‘sven2’);
alert(a===b); //true;
4.2 透明的单例模式
我们的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下面的例子中,我们将使用CreateDiv单例类,它的作用是负责在页面中创建唯一的div节点,如下:
var CreateDiv = (function(){
var instance;
var CreateDiv =function(html){
if(instance){
return instance;
}
this.html = html;
this.init();
return instance = this;
}
CreateDiv .prototype.init= function(){
var div = document.createElement(‘div’);
div.innerHTML = this.html;
document.body.appendChild(div);
};
return CreateDiv;
})();
上面的代码看上去会很难理解,在这段代码中,CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化的init方法,第二是保证只有一个对象。虽然我们目前我们还没学习到“单一职责原则”的概念,但可以明确的是,这是一种不好的做法,至少这个构造函数看起来好奇怪……,
假设我们某天需要利用这个类,在页面上创建多个div,即要让这个类从单例变成一人普通的可产生多个实例的类,那我们必须改写CreateDiv构造函数,把控制创建唯一那段去掉,这种修改会给我们带来不必要的烦恼。
4.3 用代理实现单例模式
现在我们通过引入代理方式,来解决上面提到的问题。
首先在CreateDiv构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:
var CreateDiv = function(html){
this.html = html;
this.init();
}
CreateDiv.prototype.init = function(){
var div = document.createElement(‘div’);
div.innerHTML =this.html;
document.body.appendChild(div);
}
接下来引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到代理类proxySingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。如下:
<pre name="code" class="javascript">var ProxySingletonCreateDiv =(function(){
var instance;
return function(html){
if(!instance){
instance = new CreateDiv(html);
}
return instance;
}
})();
var a = new ProxySingletonCreateDiv(‘sven1’);
var b = new ProxySingletonCreateDiv(‘sven2’);
alert(a===b); //true
4.4 javascript中的单例模式
前端提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。
js其实是一个无类语言,也正因为如此,生搬单例模式的概念并无意义。在javaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个类呢(相当有同感)?
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例,但在javascript中,我们经常会把全局变量当成单例来使用如:
var a ={};
但这种方式比较糟糕的问题是,全名冲突。维护不容易啊
解决方法有如下两种:
1. 使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
如下:
namespace1={
a:function(){
alert(1);
},
b:function(){
alert(2);
}
}
把a 和 b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。另外我们还可以动态地创建命名空间(Object-Oriented javascript)
var MyApp = {};
MyApp.namespace= function(name){
var parts = name.split(‘.’);
var current = MyApp;
for(var i in parts){
if(!current[parts[i]]){
current[parts[i]] = {};
}
current = current[parets[i]];
}
}
MyApp.namespace(‘event’);
MyApp.namespace(‘dom.style’);
consle.dir(MyApp);
//结果
{
event:{},
dom:{
style:{}
}
}
2. 全用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
var user =(function(){
var __name=’sven’,
__age = 29;
return {
getUserInfo:function(){
return __name+’-‘+__age;
}
}
})();
4.5 惰性单例
需要才创建,这种技术在实际开发时非常有用,有用的程序超出我们的想象……,如我们在前面所讲的Singleton.getInstance的实现。但javascript中并不适用(因为它是基于类的创建方式生搬硬套感觉在实际应用中真真没啥用)。
Singleton.getInstance=(function(){
var instance =null;
returnfunction(name){
if(!instance){
instance = new Singleton(name);
}
return instance;
}
})();
Demo Web QQ登录页面,当点击导航的QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。
第一种解决方案在页面加载完成的时候便创建好这个div浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示
<pre name="code" class="html"><html>
<body>
<button id=”loginBtn”>登录</button>
</body>
<script>
var loginLayer =(function(){
var div = document.createElement(‘div’);
div.innerHTML = “登录浮窗”;
div.style.display = ‘none’;
document.body.appendChild(div);
return div;
})();
document.getElementById(‘loginBtn’).οnclick= function(){
loginLayer.style.display = ‘block’;
}
</script>
</html>
这种方式的缺点就是登录这个页面,不一定是启用登录QQ界面,如我们只是看看天气,根本不需要进行登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。
那么我们将其改造一下,
<html>
<body>
<button id=”loginBtn”>登录</button>
</body>
<script>
var createLoginLayer = function(){
var div = document.createElement(‘div’);
div.innerHTML = “登录浮窗”;
div.style.display = ‘none’;
document.body.appendChild(div);
return div;
};
document.getElementById(‘loginBtn’).οnclick= function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = ‘block’;
}
</script>
</html>
在上例中虽然达到惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会创建一个新的登录浮窗div。虽然我们可以在点击浮窗上的关闭按钮时把这个浮窗从页面中删除,但这样频繁地创建和删除节点明显是很不合理的,也是不必要的。
所以可以把 createLoginLayer改成单例模式
<pre name="code" class="javascript">var createLoginLayer = (function(){
var div;
return function(){
if(!div){
div = document.createElement(‘div’);
div.innerHTML = “登录浮窗”;
div.style.display = ‘none’;
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById(‘loginBtn’).οnclick= function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = ‘block’;
}
4.6 通用的惰性单例
上一节中我们完成的一个可用的惰性单例,但是我们发现它还有如下一些问题。
o 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象的内部
o 如果我们下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍:
var createIframe = (function(){
var iframe;
return function(){
if(!iframe){
iframe =document.createElement(‘iframe’);
iframe.style.display = ‘none’;
document.body.appendChild(iframe);
}
return iframe;
}
})();
其实我是要把不变的部分隔离出来,先不考虑创建一个div还是一个iframe有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象:
var obj;
if(!obj){
obj =xx ; //bala…bala…
}
现在我们将管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingleton函数内部,创建对象的方法fn被当成参数动态传入函数:
var getSingle = function(fn){
var result;
return function(){
return result || (result =fn.apply(this,arguments))
}
}
接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。
之后再让getSingle返回一个新的函数,并且一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值。如下:
var createLoginLayer = function(){
var div = document.createElement(‘div’);
div.innerHTML = ‘我是登录窗口’;
div.style.display = ‘none’;
return div;
}
var createSingleLoginLayer =getSingle(createLoginLayer);
document.getElementById(‘loginBtn’).οnclick= function(){
var loginLayer = createSingleLoginLayer ();
loginLayer.style.display = ‘block’;
}
如我们还可以创建唯一一个iframe用于动态加载第三方页面
var createSingleIframe =getSingle(function(){
var iframe =document.createElement(‘iframe’);
document.body.appendChild(iframe);
return iframe;
});
document.getElementById(‘loginBtn’).οnclick= function(){
var loginLayer =createSingleIframe();
loginLayer.src =‘xxx’;
}
这个例子挺有意思,单例模式的用途不止用于创建对象,比如我们通常渲染完页面中一个列表之后,接下来要给列表绑定click事件,如果是通过ajax动态往列表里追回数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果我们是借助于jQuery,我们通常选择给节点绑定one事件
var bindEvent = function(){
$(‘div’).one(“click”,function(){
alert(‘click’);
});
};
var render = function(){
console.log(‘开始渲染列表’);
bindEvent();
}
render();
render();
render(); //<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
如果利用getSingle函数,也能达到一样的效果:
var bindEvent = getSingle(function(){
document.getElementById(‘div1’).onclick = function(){
alert(‘click’);
}
return true;
});
var render = function(){
console.log(‘开始渲染列表’);
bindEvent();
}
render();
render();
render();
可以看到,render函数和bindEvent函数都分别执行了3次,但div实际上只被绑定了一个事件。