详解JavaScript中的单例模式
什么是单例模式?
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在JavaScript开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
如何实现一个单例模式
要实现一个单例模式非常简单,无非使用一个变量来标志是否为某个类创造过一个实现,如果没有,就创造一个,如果有,就直接返回之前创造的那个。
function Singleton() {
this.instance = null
}
Singleton.prototype.getInstance = function() {
return this.instance || (this.instance = new Singleton())
}
var s1 = Singleton.getInstance()
通过以上代码我们实现了一个简单的单例模式,通过getInstance方法来获取单例对象,这似乎与以往获取对象和创建对象的方式有所区别,如果用以往通过new Singleton()
的方式来创建对象,那我们得到的对象还符合单例模式吗? 答案是不符合的,这段代码具有一定的“不透明性”他要求使用者在使用这段代码的时候必须通过getInstance方法来获取对象。常规的new xxx()
并不可行。
透明的单例模式
一个完美的单例模式是“透明的”,也就是说,使用者在使用的时候不需要知道单例模式的存在。 们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。在下面的例子中,我们将使用CreateDiv单例类,它的作用是负责在页面中创建唯一的div节点。
var CreateDiv = (function() {
var instance = null
var CreateDiv = function() {
if (instance) {
return instance
}
this.create()
return instance = this
}
CreateDiv.prototype.create = function() {
var div = document.createElement('div')
div.innerHTML = 'hello world'
document.body.appendChild(div)
}
return CreateDiv
})()
虽然现在完成了一个透明的单例类的编写,但它同样有一些缺点。为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
然现在完成了一个透明的单例类的编写,但它同样有一些缺点。为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
用代理实现单例模式
实际上单例模式的构造函数做了两件事情,第一件事情是创建一个对象,第二件事情是确保返回对象的唯一性。 这是每个单例模式都会做的事情,那我们就可以将这个步骤抽离出来。 我们依然使用上述代码,把管理单例模式的代码删除,使其成为一个普通的创建dom节点的类。
function CreateDiv() {
this.create()
}
CreateDiv.prototype.create = function() {
var div = document.createElement('div')
div.innerHTML = 'hello world'
document.body.appendChild(div)
}
接下来引入代理类proxySingletonCreateDiv:
var proxySingletonCreateDiv = (function() {
var instance = null
return function(Constructor,...args) {
return instance || (instance = new Constructor(...args))
}
})()
var a = new proxySingletonCreateDiv(CreateDiv)
JavaScript中的单例模式
JavaScipt是一门面向对象的语言,但是JavaScript中的面向对象却不是基于类的,JavaScript中的对象是基于原型的。这意味着JavaScript在创建一个对象时并没有实例化一个类,而是基于原型继承。因此基于类的单例模式在JavaScript中并不适用。JavaScript是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无意义。在JavaScript中创建对象的方法非常简单,既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统的单例模式实现在JavaScript中并不适用。 单例模式的核心是确保只有一个实例,并提供全局访问。
惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用,有用的程度可能超出了我们的想象。 设我们是WebQQ的开发人员,当点击导航里QQ头像时,会弹出一个登录浮窗,很明显这个浮窗在页面里总是唯一的,不可能出现同时存在两个登录窗口的情况。 第一种解决方案是在页面加载完成的时候便创建好这个div浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示:
<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' ).onclick = function(){
loginLayer.style.display = 'block'
};
</script>
</html>
这种方式有一个问题,也许我们进入WebQQ只是玩玩游戏或者看看天气,根本不需要进行登录操作,因为登录浮窗总是一开始就被创建好,那么很有可能将白白浪费一些DOM节点。 现在改写一下代码,使用户点击登录按钮的时候才开始创建该浮窗:
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' ).onclick = function(){
var loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
通用的惰性单例
上一节我们完成了一个可用的惰性单例,但是我们发现它还有如下一些问题。
- 这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部。
- 如果我们下次需要创建页面中唯一的iframe,或者script标签,用来跨域请求数据,就必须得如法炮制,把createLoginLayer函数几乎照抄一遍。
我们需要把不变的部分隔离出来,先不考虑创建一个div和创建一个iframe有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象:
var obj
if ( ! obj ){
obj = xxx
}
现在我们就把如何管理单例的逻辑从原来的代码中抽离出来,这些逻辑被封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数:
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'
document.body.appendChild( div )
return div
}
var createSingleLoginLayer = getSingle( createLoginLayer )
document.getElementById( 'loginBtn' ).onclick = function() {
var loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
参考:曾探《JavaScript设计模式与开发实践》