单例模式
单例模式的定义是:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器的window对象等。例如,当我们点击登录按钮时,页面会弹出一个登录悬浮窗,而这个登录悬浮窗是唯一的,无论点击多少次登录按钮,这个悬浮窗只会被创建一次,这时,这个悬浮窗就适合用单例模式来创建。
实现单例模式
实现一个标准的单例模式,一般是用一个变量来标志当前是否已经为某个类创建过对象,若是,则在下一次获取该类的实例时,直接返回之前创建的对象。
不透明的单例模式
var Singleton = function(name){
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function(){
console.log(this.name);
};
Singleton.getInstance = function(name){
if(! this.instance){
this.instance = new Singleton(name);
}
return this.instance;
};
var a = Singleton.getInstance('sin1');
var b = Singleton.getInstance('sin2');
console.log(a === b); // 输出:true
我们通过Singleton.getInstance来获取Singleton类的唯一对象,这种方式想对简单,但有一个问题,就是增加了类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以往通过new xxx来获取对象的方式不同,这里只能使用Singleton.getInstance来获取对象。
透明的单例模式
现在我们通过一段代码来实现一个透明的单例类,用户从这个类中创建对象的时候,可以像使用其他任何普通类一样。
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('sin1');
var b = new createDiv('sin2');
console.log(a === b); // 输出:true
为了把instance封装起来,我们使用了自执行的匿名函数和闭包,并且让这个匿名函数返回真正的Singleton构造方法,这增加了一些程序的复杂度,阅读起来也不是很舒服。
观察Singleton构造函数的代码,该构造函数实际上负责了两件事情:第一是创建对象和执行初始化init方法,第二是保证只有一个对象。这不符合设计原则中的“单一职责原则”,这是一种不好的做法。假设我们某天需要利用这个类,在页面中创建很多个div,即让这个类从单例类编程一个普通的可以产生多个实例的类,我们就得改写createDiv构造函数,把控制创建唯一对象的那一段去掉,这种修改会给我们带来不必要的烦恼。
用代理实现单例模式
现在我们通过引入代理类的方法,来解决上面提到的问题。
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
var proxySingletonCreateDiv = (function(){
var instance;
return function(html){
if(!instance){
instance = new createDiv(html);
}
return instance;
}
})();
var a = new proxySingletonCreateDiv('sin1');
var b = new proxySingletonCreateDiv('sin2');
我们把负责管理单例的逻辑移到了代理类proxySingletonCreateDiv中。这样一来,createDiv就变成了一个普通的类,它跟proxySingletonCreateDiv组合起来就可以达到单例模式的效果;如果单独使用,就作为一个普通的类,能产生多个实例对象。
JavaScript中的单例模式
前面提到的单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从类中创建而来。在以类为中心的语言中,这是很自然的做法,比如在Java中,如果需要某个对象,就必须先定义一个类,对象总是从类中创建而来。
但JavaScript是一门无类语言,生搬单例模式的概念并无意义。在JavaScript中创建对象非常简单,直接声明即可。既然这样,我们就没有必要为它先创建一个类。
单例模式的核心是确保只有一个实例,并提供全局访问。
全局变量不是单例模式,但在JavaScript开发中,我们经常会把全局变量当成单例模式来使用,例如var a = {};
。
当用这种方式创建对象a时,对象a确实独一无二。如果变量a被声明在全局作用域下,则我们可以在代码中的任何位置使用这个变量,全局变量自然能全局访问。这样就满足了单例模式的两个条件。
但是,全局变量存在一些问题:
容易造成命名空间污染;
在大型项目中,如果不加以限制和管理,程序中可能存在很多这样的变量;
JavaScript中的变量很容易被不小心覆盖。
因此,在使用全局变量时,我们要尽力降低它的污染,通过以下方式:
1.使用命名空间
适当地使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。
最简单的方法依然是用对象字面量的方式:
var namespace1 = {
a: function(){
alert(1);
},
b: function(){
alert(2);
}
};
把a和b都定义为namespace1的属性,这样可以减少变量和全局作用域打交道的机会。‘
另外,可以动态地创建命名空间,如:
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');
上述代码等价于:
var myApp = {
event:{},
dom:{
style:{}
}
};
2.使用闭包封装私有变量
这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:
var user = (function(){
var __name = 'sin1';
var __age = 29;
return {
getUserInfo: function(){
return __name + '-' + __age;
}
}
})();
我们用下划线来约定私有变量__name和__age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命名污染。
惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例在实际开发中非常有用,是单例模式的重点。
我们在开头写的Singleton类就用过这种技术,instance实例对象总是在我们调用Singleton.getInstance的时候才被创建,而不是在页面加载好的时候就创建。
实现惰性单例
假设,在一个提供登录功能(点击登录按钮弹出一个登录悬浮窗)的web页面中,可能用户在访问过程中,根本不需要进行登录操作,只需要浏览某些内容。所以,没有必要在页面加载好之后就马上创建登录悬浮窗,只需要当用户点击登录按钮的时候才开始创建登录悬浮窗,实现代码如下:
<!DOCTYPE html>
<html>
<head>
<title>惰性单例</title>
</head>
<body>
<button id = "loginBtn">登录</button>
</body>
<script type="text/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').onclick = function(){
var loginLayer = createLoginLayer();
loginLayer.style.display = 'block';
};
</script>
</html>
但这段代码还是存在一些问题的:
这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在createLoginLayer对象内部;
如果我们下次需要创建页面中唯一的iframe,或者script标签,必须得如法炮制,把createLoginLayer函数几乎照抄一遍。
通用的惰性单例
为了解决上面的问题,我们可以实现一段通用的惰性单例代码:
<!DOCTYPE html>
<html>
<head>
<title>惰性单例</title>
</head>
<body>
<button id = "loginBtn">登录</button>
</body>
<script type="text/javascript">
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';
};
</script>
</html>
上面的代码,
把管理单例的逻辑抽象了出来:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象;
把如何管理单例的逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数;
将创建登录悬浮窗的方法传入getSingle,还能传入createIframe,createScript;
getSingle函数返回一个新的函数,并且用一个变量result来保存fn的计算结果,result变量在闭包中,永远不会被销毁,所以在将来的请求中,如果result已经被赋值,那么它将返回这个值。
单例模式的用途不止在于创建对象,比如我们通常渲染完页面中的一个列表后,就要给这个列表绑定click事件,如果通过ajax动态往列表里追加数据,在使用事件代理的前提下,click事件实际上只需要在第一次渲染列表的时候就被绑定一次。
<!DOCTYPE html>
<html>
<head>
<title>惰性单例</title>
</head>
<body>
<button id = "renderBtn">渲染列表</button>
</body>
<script type="text/javascript">
var getSingle = function(fn){
var result;
return function(){
return result || (result = fn.apply(this, arguments));
}
};
var bindEvent = getSingle(function(){
console.log('绑定click事件');
document.getElementById('renderBtn').onclick = function(){
alert('click');
}
return true;
});
var render = function(){
console.log('开始渲染');
bindEvent();
}
render();
render();
render();
// 最终输出结果:
// 开始渲染
// 绑定click事件
// 开始渲染
// 开始渲染
</script>
</html>
PS:本节内容为《JavaScript设计模式与开发实践》第四章 笔记。