单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
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; } })(); var a = Singleton.getInstance('sven1'); var b = Singleton.getInstance('sven2'); alert(a === b); //true
我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式相对简单,但是有一个问题,增加了这个类的"不透明性"。
这段单例模式代码的意义并不大。
2.透明的单例模式
我们现在的目标是实现一个"透明"的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。
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; })(); var a = new CreateDiv('sven1'); var b = new CreateDiv('sven2'); alert(a === b);
但它同样有一些缺点。
CreateDiv的构造函数实际上负责了两件事情。第一是创建对象和执行初始化方法init方法,第二是保证只有一个对象。违背了"单一职责原则"。
3.用代理实现单例模式
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); }; //引入代理类 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);
通过引用代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例模式的逻辑移到了代理类ProxySingletonCreateDiv中。这样一来,CreateDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来可以达到单例模式的效果。
4.JavaScript中的单例模式
单例对象从"类"中创建而来。在以类为中心的语言中,这是很自然的做法。但JavaScript是一门无类(class-free)语言,也正因为如此,生搬单例模式的概念并无意义。
单例模式的核心是确保只有一个实例,并提供全局访问。
全局模式不是单例模式,但在JavaScript开发中,我们经常会把全局变量当成单例模式来使用。
var a = {};
这种方式创建对象a时,对象a确实是独一无二的。如果a变量被声明在全局作用域下,则我们可以在代码中任何位置使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。
但是全局变量很容易造成命名空间污染。还有变量冲突。
作为普通的开发者,我们有必要尽量减少全局变量的使用,即使需要,也要把它的污染降到最低。
以下几种方式可以相对降低全局变量带来的命名污染。
1.使用命名空间
最简单的方法是用对象字面量的方式。
var namespace1 = { a: function(){ alert(1); }, b: function(){ alert(2); } };
还可以动态的创建命名空间。
//动态创建命名空间 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[parts[i]]; } }; MyApp.namespace('event'); MyApp.namespace('dom.style'); console.dir(MyApp); //上述代码等价于 var MyApp = { event:{}, dom:{ style:{} } };
2.使用闭包封转私有变量
把一些变量封转在闭包的内部,只暴露一些接口跟外界通信。
5.惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实际开发中非常有用,有用的程度可能超出了我们的想象。
以WebQQ的登录浮窗为例,介绍与全局变量结合实现惰性的单例。
<body> <button id="loginBtn">登录</button> </body> </html> <script> var createLoginLayer = (function(){ var div; return function(){ if(!div){ div = document.createElement('div'); div.innerHTML = '我是登录浮窗'; document.body.appendChild(div); } return div; } })(); document.getElementById('loginBtn').onClick = function(){ var loginLayer = createLoginLayer(); loginLayer.style.display = 'block'; }; </script>
上段代码仍然违反单一职责原则,创建对象和管理单例的逻辑都放在createLoginLayer对象内部。
//改进版 var getSingle = function(fn){ var result; return function(){ return result || (result = fn.apply(this,arguments)); } }; 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'; }; //创建唯一的iframe用于动态加载第三方页面 var createSingleIframe = getSingle(function(){ var iframe = document.createElement('iframe'); document.body.appendChild(iframe); return iframe; }); document.getElementById('loginBtn').onClick = function(){ var loginLayer = createSingleIframe(); loginLayer.src = 'http://baidu.com'; };
在这个例子中,我们把创建实例对象的职责和管理单例的职责分别放置在两个方法里,这两个方法可以独立变化而互不影响,当它们连接在一起的时候,就完成了创建唯一实例对象的功能,看起来是一件挺奇妙的事情。
小结
单例模式是一种简单但非常有用的模式,特别是惰性单例模式,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。