第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来获取对象。
4.2 透明的单例模式
我们的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下面的例子中,我们将使用CreateDiv单例类,它的作用是负责在页面中创建唯一的div节点,如下:
上面的代码看上去会很难理解,在这段代码中,CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化的init方法,第二是保证只有一个对象。虽然我们目前我们还没学习到“单一职责原则”的概念,但可以明确的是,这是一种不好的做法,至少这个构造函数看起来好奇怪……,
假设我们某天需要利用这个类,在页面上创建多个div,即要让这个类从单例变成一人普通的可产生多个实例的类,那我们必须改写CreateDiv构造函数,把控制创建唯一那段去掉,这种修改会给我们带来不必要的烦恼。
4.3 用代理实现单例模式
现在我们通过引入代理方式,来解决上面提到的问题。
首先在CreateDiv构造函数中,把负责管理单例的代码移除出去,使它成为一个普通的创建div的类:
接下来引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例的逻辑移到代理类proxySingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。如下:
4.4 javascript中的单例模式
前端提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象“类”中创建而来。在以类为中心的语言中,这是很自然的做法。比如在Java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来的。
js其实是一个无类语言,也正因为如此,生搬单例模式的概念并无意义。在JavaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个类呢(相当有同感)?
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例,但在javascript中,我们经常会把全局变量当成单例来使用如:
var a ={};
但这种方式比较糟糕的问题是,全名冲突。维护不容易啊
解决方法有如下两种:
1. 使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
如下:
把a 和 b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。另外我们还可以动态地创建命名空间(Object-Oriented javascript)
2. 全用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
4.5 惰性单例
需要才创建,这种技术在实际开发时非常有用,有用的程序超出我们的想象……,如我们在前面所讲的Singleton.getInstance的实现。但javascript中并不适用(因为它是基于类的创建方式生搬硬套感觉在实际应用中真真没啥用)。
Demo Web QQ登录页面,当点击导航的QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。
第一种解决方案在页面加载完成的时候便创建好这个div浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示
这种方式的缺点就是登录这个页面,不一定是启用登录QQ界面,如我们只是看看天气,根本不需要进行登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。
那么我们将其改造一下,
在上例中虽然达到惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会创建一个新的登录浮窗div。虽然我们可以在点击浮窗上的关闭按钮时把这个浮窗从页面中删除,但这样频繁地创建和删除节点明显是很不合理的,也是不必要的。
所以可以把 createLoginLayer改成单例模式
4.6 通用的惰性单例
上一节中我们完成的一个可用的惰性单例,但是我们发现它还有如下一些问题。
o 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象的内部
o 如果我们下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍:
其实我是要把不变的部分隔离出来,先不考虑创建一个div还是一个iframe有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象:
现在我们将管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingleton函数内部,创建对象的方法fn被当成参数动态传入函数:
接下来将用于创建登录浮窗的方法用参数fn的形式传入getSingle,我们不仅可以传入createLoginLayer,还能传入createScript、createIframe、createXhr等。
之后再让getSingle返回一个新的函数,并且一个变量result来保存fn的计算结果。result变量因为身在闭包中,它永远不会被销毁。在将来的请求中,如果result已经被赋值,那么它将返回这个值。如下:
如我们还可以创建唯一一个iframe用于动态加载第三方页面
这个例子挺有意思,单例模式的用途不止用于创建对象,比如我们通常渲染完页面中一个列表之后,接下来要给列表绑定click事件,如果是通过ajax动态往列表里追回数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是否是第一次渲染列表,如果我们是借助于jQuery,我们通常选择给节点绑定one事件
如果利用getSingle函数,也能达到一样的效果:
可以看到,render函数和bindEvent函数都分别执行了3次,但div实际上只被绑定了一个事件。