JS设计模式——单例模式

前言

本系列为阅读曾探的《JavaScript设计模式与开发实践》一书所做的读书笔记,大部分内容摘自原书,加入了部分个人理解。

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式在 js 开发中的应用场景:登录弹窗(一个网站只应该有一个登录弹窗,多次点击也不会出现多个)。

Java语言下单例模式的实现(饿汉单例模式)

1
2
3
4
5
6
7
public class Singleton {
private static Singleton instance = new Singleton(){};
private Singleton(){};
public static Singleton getInstance() {
return instance;
}
}

JS 语言下单例模式的实现

模仿 java 语言的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
function Singleton() {
this.name = 'singleton';
this.instance = null;
}
Singleton.getInstance = function() {
if(!this.instance) {
this.instance = new Singleton();
}
return this.instance;
}
var a = Singleton.getInstance();
var b = Singleton.getInstance();
a === b; //true

​其中,getInstance 方法也可以使用闭包实现。

1
2
3
4
5
6
7
8
9
10
11
12
var Singleton = function( name ){
this.name = name;
};
Singleton.getInstance = (function(){
var instance = null;
return function( name ){
if ( !instance ){
instance = new Singleton( name );
}
return instance;
}
})();

​ 这种方法缺点很明显,使用者必须知道 Singleton 是单例类,并且获取对象不能通过 new 的方式。

透明化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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' );
a === b; // true

通过自执行的匿名函数返回 Singleton 构造方法,并且使用闭包保证了 instance 的唯一性。

但这样的代码比较复杂,并且 CreateDiv 的构造函数负责了创建对象、保证 instance 唯一两个功能,违背了单一职责原则。

使用代理实现单例模式

从上述代码中移除管理单例的代码:

1
2
3
4
5
6
7
8
9
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 );
};

引入代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
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' );
a === b; //true

这也是缓存代理的一个应用。

js 语言的单例模式

前面几种单例模式的实现方式,是接近传统面向对象语言的实现,从“类”中创建单例对象。但 JavaScript 其实是一门无类(class-free)语言,所以不需要像 java 等面向对象语言的方式来实现单例模式。

单例模式的核心是确保只有一个实例,并提供全局访问。在JavaScript 中创建对象的方法非常简单,既然只需要一个“唯一”的对象,那么不需要为它先创建一个“类”。

在原书中,使用全局变量当作单例使用,例如:

1
var a = {};

但是全局变量很容易造成命名空间污染。书中介绍了几种可以相对降低全局变量带来的命名污染问题的方式:

使用命名空间

最简单的方法依然是用对象字面量的方式,减少全局变量的数量:

1
2
3
4
5
6
7
8
var namespace1 = {
a: function(){
alert (1);
},
b: function(){
alert (2);
}
};

动态地创建命名空间(引自Object-Oriented JavaScrtipt 一书):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 );

使用闭包封装私有变量

把一些变量封装在闭包的内部,只暴露一些接口跟外界通信:

1
2
3
4
5
6
7
8
9
var user = (function(){
var __name = 'sven',
__age = 29;
return {
getUserInfo: function(){
return name + '-' + age;
}
}
})();

个人认为在支持 ES6 的条件下,可以使用 let 声明变量,进一步保证对象的唯一性,避免被覆盖的问题。

惰性单例

惰性单例指的是在需要的时候才创建对象实例,如上述 js 实现单例模式的简单方法。
举例,登录浮窗的实现,点击按钮后才生成浮窗,并且一个页面只能存在一个。
实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 管理单例的逻辑,创建单例的方法作为参数 fn 动态传入
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;
};
// 通过 getSingle 可以实现不同的创建需求
var createSingleLoginLayer = getSingle(createLoginLayer);
// 惰性创建
document.getElementById('loginBtn').onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};

单例模式还可以应用在 click 事件单次绑定。这里要注意书中对 jQuery 的 one 事件描述是不正确的,one 事件实现的是单次响应事件,而不是单次绑定事件。