单例模式

什么是单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就是单例模式,也是单例模式的定义。

单例模式的用途非常广,像实现全局模态框,像在Redux和Vuex中的Store实现,就是单例模式的典型应用。

实现简单的单例模式

实现一个简单的单例模式并不复杂,核心知识点在于使用一个变量来标志这个类是否已经创建过对象实例,如果是,则直接返回之前创建的对象实例,如果为否,则创建这个类的对象实例,并保存到这个变量标志中。

代码实现

ES5实现

完整代码

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

测试用例

1
2
3
let a = Singleton.getInstance( 'sven1' );
let b = Singleton.getInstance( 'sven2' );
console.log ( a === b ); // true

ES6实现

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 单例模式
*/
class Singleton {
constructor(name) {
this.name = name;
this.instance = null; // 单例模式的标识
}

getName() {
console.log(this.name);
}
/**
* 获取单例的静态方法
*/
static getInstance(name) {
// 如果已经创建过对象实例,则直接返回之前创建的对象实例,如果没有,则创建这个类的对象实例,并保存到instance这个单例变量标识中
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
}

测试用例

1
2
3
let a = Singleton.getInstance( 'sven1' );
let b = Singleton.getInstance( 'sven2' );
console.log ( a === b ); // true

透明的单例模式

上面的例子中,我们实现了简单的单例模式,虽然这种方法使用起来很简单,但是对使用者而言,需要提前知道这个类是一个**”单例类”,才知道该使用getInstance**来获取对象实例。

为了实现单例模式的”透明”特性,在ES5中,我们要用上自执行匿名函数和闭包,利用闭包存储公共的单例类实例,使用自执行匿名函数返回真正的单例类的构造方法;而在ES6中,我们可以利用类的构造器返回公共的单例类实例。

代码实现

ES5实现

完整代码

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
26
/**
* 透明单例模式
* @param {*} html
*/
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;

})();

测试用例

1
2
3
4
5
var a = new CreateDiv( 'sven1' );
var b = new CreateDiv( 'sven2' );

console.log(a === b);
// true

ES6实现

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 透明单例模式
*/
class CreateDiv {
constructor(html) {
if (!CreateDiv.instance) {
CreateDiv.instance = this;
this.html = html;
this.init();
}
return CreateDiv.instance;
}

init () {
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
}

测试用例

1
2
3
4
5
let a = new CreateDiv( 'sven1' );
let b = new CreateDiv( 'sven2' );

console.log(a === b);
// true

用代理实现单例模式

虽然上述这种透明的单例类写法可以让使用者直接使用new关键字的获取单例类实例,但是一定程度增加了程序阅读的复杂度,还有可能破坏了”单一职责原则”。如果以后需要为其他类实现透明的单例,就要重复为这个类重新实现一遍透明单例类,这怎么看都不是一种很好的解决方法。

利用ES6的代理(proxy),来解决这个问题,就可以让单例实现和类本身进行解耦。在ES5中,由于并不支持代理(proxy)的特性,我们需要自己实现一个代理类。

代码实现

ES5实现

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var CreateElement = function( html ){
this.html = html;
this.init();
};
CreateElement.prototype.init = function(){
var div = document.createElement( 'div' );
div.innerHTML = this.html;
document.body.appendChild( div );
};
var ProxySingletonCreateElement = (function(CreateElement){
var instance;
return function(){
if (!instance) {
// 为了便捷,这里使用ES6才支持的扩展运算符,也可以使用类似eval的方式模拟
instance = new CreateElement(...arguments)
}
return instance;
}
})(CreateElement);

测试用例

1
2
3
4
var a = new ProxySingletonCreateElement( 'sven1' );
var b = new ProxySingletonCreateElement( 'sven2' );
console.log ( a === b );
// true

ES6实现

完整代码

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
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 透明单例模式
*/
class CreateDiv {
constructor(html) {
this.html = html;
this.init();
}
init() {
let div = document.createElement("div");
div.innerHTML = this.html;
document.body.appendChild(div);
}
}

function singletonProxy(className) {
let instance = null;
return new Proxy(className, {
/**
* @description construct()方法用于拦截new命令,可以接受三个参数
* @param target 目标对象
* @param args 构造函数的参数数组
* @param newTarget 创造实例对象时,new命令作用的构造函数
*/
construct(target, args) {
// 内部类
class ProxyClass {
constructor() {
if (instance === null) {
instance = new target(...args);
}
return instance;
}
}
return new ProxyClass();
},
});
}

测试用例

1
2
3
4
5
let createSothx = singletonProxy(CreateDiv)
let sothx1 = createSothx('sothx1')
let sothx2 = createSothx('sothx2')
console.log(sothx1 === sothx2)
// true

JavaScipt中的单例模式

对于传统面向对象语言而言,单例对象是从”类”中创建而来的,比如Java中,如果需要某个对象,就必须先定义一个类,对象从类中被创建出来。

但是对于JavaScript而言,其实是一门无类(class-free)语言,因此在JavaScript中创建对象的方法特别简单,只需要保证单例模式的核心——确保只有一个实例,并提供全局访问。

那么他就是一个单例类。

直接在全局上下文定义一个对象字面量的全局变量,也可以把它看做单例类,但是全局变量很容易造成命名空间污染的问题。

1
2
// 全局上下文的a对象,它也可以看做是一个单例类。
let a = {};

模拟命名空间,可以减少全局变量造成污染的概率。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 对象字面量的方法模拟命名空间
*/
let namespace1 = {
a: function(){
console.log(1);
},
b: function(){
console.log(2);
}
};
/**
* 动态创建命名空间
*/
let 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 );

// 上述代码等价于:

let MyApp = {
event: {},
dom: {
style: {}
}
};

/**
* 使用闭包封装私有变量
*/
let user = (function(){
let __name = 'sven',
__age = 29;

return {
getUserInfo: function(){
return __name + '-' + __age;
}
}

})();

在ES6中,我们可以模块(module)来模拟命名空间。

  1. 创建一个模块文件
1
2
3
4
5
6
7
8
9
10
// 模块文件
// MyApp.js
let MyApp = {
event: {},
dom: {
style: {}
}
};

export default MyApp;
  1. 页面中引入模块
1
<script type="module" src="./MyApp.js"></script>
  1. 需要使用该模块的js文件中导入模块并使用
1
2
3
import MyApp from './MyApp.js';

console.log(MyApp)

懒汉式和饿汉式

单例模式的写法有很多,懒汉式,饿汉式,注册式,序列化式等等……

这里着重对懒汉式单例和饿汉式单例做一个比较讲解。

  • 懒汉式:默认不会实例化,什么时候用到了,才会创建对象实例。
  • 饿汉式:类加载的时候就会进行实例化,并且创建单例对象实例。

像先前的例子,Singleton.getInstance在调用的时候才会被创建,这种就是典型的懒汉式单例。

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

用在实际的例子,比如创建一个登录弹框,我们在JavaScript也可以这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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';
};

通用的懒汉式单例

上面的登录弹框写法虽然可用,但是违反了设计模式的”单一职责原则”,创建对象和管理单例的逻辑都耦合在了一起。如果之后要创建更多的单例,就要重复复制粘贴一遍又一遍单例代码。

所以我们需要把管理单例的逻辑从原来的代码中抽离出来。

1
2
3
4
5
6
let getSingle = function( fn ){
let result; // 保存在闭包中,不会被销毁。
return function(){
return result || ( result = fn.apply(this, arguments ) );
}
};

接着,无论有多少种不同的创建实例对象的方法,getSingle这个管理管理方法都能很好的完成任务。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 例子1:创建登录弹窗,保证全局只有一个登录弹窗
*/
// 创建登录弹窗的方法
let createLoginLayer = function(){
var loginDiv = document.createElement( 'div' );
loginDiv.innerHTML = '我是登录浮窗';
loginDiv.style.display = 'none';
document.body.appendChild( loginDiv );
return loginDiv;
};
// 获取创建登录弹窗方法的单例
var createSingleLoginLayer = getSingle( createLoginLayer );

document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer(); // 使用这个单例
loginLayer.style.display = 'block';
};
/**
** 例子2:创建iframe,保证全局只有一个iframe
*/
// 创建iframe的单例方法
let createSingleIframe = getSingle( function(){
let iframe = document.createElement ('iframe');
document.body.appendChild(iframe);
return iframe;
});

document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleIframe(); // 使用这个单例
loginLayer.src = 'http://baidu.com';
};
/**
** 例子3:给列表绑定click事件,保证列表只在初始化后执行一次事件绑定
*/
// 绑定点击事件的方法
let bindEvent = getSingle(function(){
document.getElementById( 'div1' ).onclick = function(){
console.log ( 'click' );
}
return true;
});

var render = function(){
console.log( '开始渲染列表' );
bindEvent();
};

// 虽然执行了3次render,但是绑定事件的操作只执行了一次。
render();
render();
render();

参考资料

《JavaScript设计模式与开发实践》——单例模式

https://www.ituring.com.cn/book/1632

Proxy代理实现单例模式

https://juejin.cn/post/6844903929885507598


单例模式
https://sothx.com/2021/06/23/Singleton/
作者
Sothx
发布于
2021年6月23日
许可协议