设计模式之单例模式

单例模式是最简单也是最常用的模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的特点:

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例

主要解决:一个全局使用的类频繁地创建与销毁

怎么方便理解和记忆这种模式呢?

用一句话来记忆它就是:只有一个实例,有一个访问它的全局访问点,不能与new关键字一起使用

在JavaScript中一个对象字面量可以认为是一个最简单的单例类,以为它符合单例类的特点:只有一个实例,有一个全局访问点。如:

1
2
3
4
5
6
7
8
var Singleton = {
attribute: true,
method: function () {
//do something
}
};

Singleton.name = false;

上面示例的单例对象可以被修改,你可以随意添加属性和方法到对象中,又或者用delete运算符删除现有的成员。这实质上是违背了面向对象设计的一个原则:类可以被扩展,但不应该被修改。传统意义上的单例模式的定义是:单例类仅有一个实例,并提供一个访问它的全局访问点。上面的对象字面量不是一个实例化的类,所以严格来说,它不属于单例类。javascript不是一门传统的语言,所以不必一定要按传统的定义来限定它,我们将单例模式的定义更广义化:单例类是一组相关的属性和方法的集合,如果它能被实例化,那么它只能被实例化一次。这样对象字面量就符合单例模式的定义了。

拥有私有成员的单例类

现在一个对象字面量就是JavaScript中最简单的单例类,那怎么实现单例类的私有成员呢?私有成员是对象内部独有的、其他对象无法访问的成员。在JavaScript中实现私有成员的方法是使用闭包,闭包是高阶函数的一种应用。闭包实现单例类如下:

1
2
3
4
5
6
7
8
9
10
11
12
var Singleton = (function(){
var name = 'singleton';

return {
attribute: true,
getName: function () {
return name
}
}
})();

Singleton.getName(); //singleton

这样就实现了name成员的私有化,即在Singleton外无法直接访问和修改name,只能通过Singleton的getName方法访问name。

使用构造函数实现单例类

在JavaScript中没有类,但可以使用构造函数模拟类的行为,下面使用构造函数实现单例类:

1
2
3
4
5
6
7
function Singleton (name) {
if (!Singleton.instance) {
this.name = name
Singleton.instance = this
}
return Singleton.instance
}

或者,使用闭包把instance封装起来:

1
2
3
4
5
6
7
8
9
10
11
12
var Singleton = (function () {
var instance;

var Singleton = function (name) {
if (!instance) {
this.name = name;
instance = this;
}
return instance;
}
return Singleton;
})();

对该对象进行两次实例化,观察两次实例化结果是否指向同一个对象:

1
2
3
var a = new Singleton('a')
var b = new Singleton('b')
console.log(a === b); // true

结果是:true。说明a、b之间是引用关系。

在这段代码中,Singleton构造函数实际上负责了两件事情:第一是创建对象,第二是保证只有一个对象。很明显,这违背了“单一职责原则”,这是一种不好的做法。

用代理实现单例模式

通过引入代理,把创建对象和保证只有一个对象两个职责分离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Singleton = function (name) {
this.name = name
}

// 代理
var ProxySingleton = (function () {
var instance;

return function (name) {
if (!instance) {
instance = new Singleton(name);
}
return instance;
}
})()

创建两个实例,测试单例类:

1
2
3
var a = new ProxySingleton('a')
var b = new ProxySingleton('b')
console.log(a === b) // true

通过引入代理,实现了单例模式,并且职责清晰。但是现在还是有问题,引入代理后,多了一个全局变量(ProxySingleton),这样也不好。

在JavaScript中函数是一等对象,可以作为参数传递到函数内部执行,这是高阶函数的另一种应用。通过向立即执行函数传参,消除多余的全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
var Singleton = (function (Constructor) {
var instance;

return function (name) {
if (!instance) {
instance = new Constructor(name);
}
return instance;
}
})(function Singleton (name) {
this.name = name
})

ES6实现单例模式

ES6中创建对象时引入了class和constructor用来创建对象。用ES6实现单例模式:

1
2
3
4
5
6
7
8
9
class Singleton {
constructor (name) {
if (!Singleton.instance) {
this.name = name
Singleton.instance = this
}
return Singleton.instance
}
}

或者用代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Singleton = (function (Constructor) {
var instance;

return function (name) {
if (!instance) {
instance = new Constructor(name);
}
return instance;
}
})(class Singleton {
constructor(name) {
this.name = name
}
})

ES6的class只不过是一个语法糖,与ES5的构造函数实现单例类没什么区别。

惰性单例

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实
际开发中非常有用。

接下来我们使用惰性单例实现弹框,在用户点击登录按钮的时候才创建登录框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var createDiv = function() {
var div;

return function() {
if(!div){
div = document.createElement("div");
div.innerHTML = '我是登录框';
document.body.appendChild(div);
}
return div;
}
};

document.querySelector(".btn").onclick = createDiv();

把上面创建div的过程提取出来,便得到一个通用的惰性单例模式:

1
2
3
4
5
6
7
var createSingleton = function(fn) {
var singleton;

return function() {
return singleton || (singleton = fn.apply(this, arguments));
}
};

现在用通用的惰性单例改造下前面登录框的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var createSingleton = function(fn){
var singleton;

return function(){
return singleton || (singleton = fn.apply(this, arguments));
}
};

function createDiv(){
var div = document.createElement("div");

div.innerHTML = '我是登录框';
document.body.appendChild(div);
return div;
};

var createDivSingleton = createSingleton(createDiv);

document.querySelector(".btn").onclick = function(){
var div = createDivSingleton();
};

总结

在JavaScript实现单例模式的过程中,实际上也用了闭包和高阶函数的概念。单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。