JavaScript设计模式 -- 单例模式
作者: DocWhite白先生
一.概念
单例模式的定义是保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式是一种常用的模式,例如线程池、全局缓存、浏览器中的window对象等。
下面是一个简单例子:
function singleton(name) {
this.name = name;
this.instance = null;
}
singleton.prototype.getName = function() {
return this.name;
}
singleton.getInstance = function(name) {
return this.instance === null? new singleton(name) : this.instance;
}
const alex = singleton.getInstance('alex');
const john = singleton.getInstance('john');
console.log(alex === john); // true
这个简单的单例模式存在一个问题,就是这个单例类的使用者必须知道它是单例类,跟通过new XXX的方式获取对象不同,这里偏要使用singleton.getInstance来获取单例实例。
所以这时候需要一个透明的单例模式,可以像使用其他任何类一样从这个类中创建对象的时候返回单例对象。
二. 透明的单例模式
要想用new XXX的形式创建单例模式的实例,只需要巧用立即执行函数。
const singleton = (function() {
let instance = null;
const __singleton = function(name) {
if(instance){
return instance;
}
this.name = name;
return instance = this;
}
return __singleton;
}) ()
const alex = new singleton('alex');
const john = new singleton('john');
console.log(alex === john); // true
虽然这样实现了透明的单例模式,以让我们能够以new 命令创建单例,但是立即执行函数返回一个真正的__singleton构造方法,这增加了一些程序复杂性,同时该构造方法实际上负责了两件事情,第一个是初始化实例属性name,二是保证只有一个对象,虽然没有接触过“单一职责原则”的概念,但这是一种不好的做法。
假设未来需要把这个单例类变成一个可以产生多个实例的类,那我们必须改写__singleton这个构造函数,把控制创建唯一对象的那段去掉。
有没有更好的实现方式呢?有!
三. 用代理实现单例模式
修改一下上一个例子中的代码
const singleton = function(name) {
this.name = name;
}
const SingletonProxy = (function() {
let instance;
return function(name) {
if(!instance){
instance = new singleton(name);
}
return instance;
}
})();
const alex = new SingletonProxy('alex');
const john = new SingletonProxy('john');
console.log(alex === john); // true
通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们 把负责管理单例的逻辑移到了代理类 SingletonProxy 中。这样一来,_singleton 就变成了 一个普通的类。它跟SingletonProxy配合起来使用才达到单例模式的效果。
四. Javascript 中的单例模式。
JavaScript跟传统的面向对象语言相比,JavaScript是一门无类语言(class-free),既然我们知道单例对象是从单例“类”中创建而来,这在传统面向对象语言中,是很自然的,但是JavaScript中创建对象是一件非常简单的事情,既然我们需要一个“唯一”对象,为什么要先创建“类”,所以实质上在JavaScript中,我们常通过创建一个全局变量当做单例来使用。
//在 this === window 的环境下
var singleton = {};
这个singleton变量实际上已经满足了单例模式的两个条件,可以代码的任何位置使用这个变量,以及提供了一个全局访问的方法(当然)。但是全局变量存在一个问题,这会导致内存泄漏、命名空间污染等问题,而且 变量很容易会被别人覆盖。
即使是JavaScript的设计者本人Brandan Eich也承认全局变量是设计上的失误,是在没有足够时间思考一些东西的情况下导致的结果。
作为开发者,我们可以使用以下几个方法降低或减少全局变量的使用,即使使用它也要把它的污染降到最低。
1. 使用命名空间
var myObject = {
namespace: 'namespace1',
name: null,
a: function() {
console.log(this.namespace)
},
b: function(name) {
this.name = name;
}
}
把a、b函数定义为myObject的属性,这样就人为的减少了a、b和全局变量打交道的机会。
2. 用闭包封装私有变量
这种方法把一些变量封装在闭包的内部, 只暴露一些接口跟外界通信:
var user = (function() {
var __name = 'alex', __age = 24;
return {
getUserInfo: function() {
return __name + '-' + __age;
}
}
})();
// __name, __age被封装在闭包产生的作用域中,外部无法访问。避免了对全局的命令污染。
3. 惰性单例
惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实 际开发中非常有用,有用的程度可能超出了我们的想象,实际上在本章开头就使用过这种技术, instance 实例对象总是在我们调用 singleton.getInstance 的时候才被创建,而不是在页面加载好 的时候就创建。
但是这是基于“类”的单例模式,前面说过基于“类”的单例模式在JavaScript中并不适用。
以一个全局弹窗例子作为示例:
var createModal = function(children) {
var div = document.createElement('div');
if(children){
div.appendChild(children);
}
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
document.getElementById('openModal').addEventListener('click', function() {
var children = document.createElement('h1');
children.innerHTML = 'this is modal children!';
var modal = createModal(children);
modal.style.display = 'block';
});
虽然实现了惰性的目的,但是上面这个例子失去了单例的效果,总不能每次点击打开弹窗的按钮都创建一次单床,关闭的时候删除,这样频繁的创建和删除节点明显不合适,也是不必要的。
这时候可以用一个变量来判断是否已经创建过弹窗,这也是文章一开始第一段代码使用的方法,改造一下createModal方法:
var createModal = (function() {
var modal = null;
return function(children) {
if(!div){
div = document.createElement('div');
if(children){
div.appendChild(children);
}
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})()
4. 通用的单例
上面的例子虽然完成了一个可用的惰性单例,但是还有一些问题:
- 这段代码违反了“单一职责原则”,创建对象和管理单例都在createModal对象内部。
- 如果下次需要创建的不再是弹窗,而是iframe或者img等,相结合如法炮制把createModal照抄一遍。
这时候就需要把不变的部分隔离出来,先不考虑创建一个div和创建img有多少差异,管理单例的逻辑完全可以抽象出来,并且这个逻辑始终是一样的:用一个变量去标识是否创建过对线个,如果是则在下次直接返回这个已经创建过的对象:
var getSingleton = function() {
var instance;
return function(fn) {
return instance || ( instance = fn.apple( this, arguments ) );
}
}
// 接下来将用于创建弹窗的方法用参数fn的形式传入getSingleton,这样就可以通过动态的传fn调整这个单例的适用场景
var createModal = function(children) {
var div = document.createElement('div');
if( children ){ div.appendChild(children); }
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
var createSingleModal = getSingleton(createModal);
document.getElementById('openModal').addEventListener('click', function() {
var h1 = document.createElement('h1');
h1.innerHTML = "I'm Modal!";
var modal = createSingleModal(h1);
modal.style.display = ’block‘;
})
这种单例模式的用途远不止创建对象,比如我们通常渲染完页面中的一个列表之后,接下来 9 要给这个列表绑定 click 事件,如果是通过 ajax 动态往列表里追加数据,在使用事件代理的前提
下,click 事件实际上只需要在第一次渲染列表的时候被绑定一次,但是我们不想去判断当前是 否是第一次渲染列表。
结语
单例模式是一种简单但非常实 用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的 是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模 式的威力。