单例模式
单例模式
单例模式
概念
确保一个类只有一个实例,并提供全局访问。
单例模式是创建型设计模式的一种。针对全局仅需一个对象的场景,如线全局缓存、window 对象等。
在 JavaScript 开发中,单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少 次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
单例模式要求一个 “唯一” 和 “全局访问” 的对象,在 JavaScript 中类似全局对象,刚好满足单例模式的两个特点:“唯一” 和 “全局访问”。虽然它不是正规的单例模式,但不可否认确实具备类单例模式的特点。
模式实现
使用一个变量存储类实例对象(值初始为 **null/undefined** )。进行类实例化时,判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。多次调用类生成实例方法,返回同一个实例对象。
实例
全局变量实现单例
1
2
3
4
5
6
7
8
9
10
11
let instance = null;
let getInstance = function (arg) {
if (!instance) {
instance = arg;
}
return instance;
}
let a = getInstance('a');
let b = getInstance('b');
console.log(a === b); // true
虽然这种全局变量可以实现单例,但因其自身的问题,不建议在实际项目中将其作为单例模式的应用,特别是中大型项目的应用中,全局变量的维护成本高。
构造函数实现单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Singleton(name) {
this.name = name;
this.instance = null;
}
Singleton.getInstance = function(name) {
if (!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
let a = Singleton.getInstance('a');
let b = Singleton.getInstance('b');
console.log(a===b); // true
这种方式也有缺点,就是我们必须调用 getInstance 来创建对象,一般我们创建对象都是利用new操作符。
透明的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Singleton = (function () {
let instance;
return function (name) {
if (instance) {
return instance;
}
this.name = name;
return instance = this;
}
})()
let a = new Singleton('a');
let b = new Singleton('b');
console.log(a === b); // true
这种方法也有缺点:不符合单一职责原则,这个对象其实负责了两个功能:单例操作和创建对象。
单一职责原则:一个程序或一个类或一个方法只做好一件事,如果功能过于复杂,我们就拆分开,每个方法保持独立,减少耦合度。
“代理版”单例模式
通过“代理”的形式,意图解决:将管理单例操作,与对象创建操作进行拆分,实现更小的粒度划分,符合“单一职责原则”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function People(name) {
this.name = name;
}
let Singleton = (function () {
let instance;
return function (name) {
if (instance) {
return instance
}
return instance = new People(name);
}
})()
let a = new Singleton('a')
let b = new Singleton('b')
console.log(a === b); // true
这种方法也有缺点:代码不能复用。如果我们有另外一个对象也要利用单例模式,那我们不得不写重复的代码
通用的单例模式
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
function Singleton(Obj) {
let instance;
return function () {
if (instance) {
return instance;
}
return instance = new Obj(arguments);
}
}
function People(name) {
this.name = name;
}
function Animal(name) {
this.name = name;
}
let peopleSingleton = Singleton(People);
let animalSingleton = Singleton(Animal);
let a = new peopleSingleton('a');
let b = new peopleSingleton('b');
console.log(a === b);
let A = new animalSingleton('A');
let B = new animalSingleton('B');
console.log(A === B);
这种方法就可以复用单例 Singleton 代码。
ES6单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
class People {
constructor(name) {
if (!People.instance) {
this.name = name;
People.instance = this;
}
return People.instance;
}
}
const a = new People('a');
const b = new People('b');
console.log(a === b); // true
开发案例
实现一个点击登录按钮弹出登录框的例子
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.dialog {
width: 200px;
height: 200px;
text-align: center;
line-height: 200px;
border: 1px solid red;
}
</style>
</head>
<body>
<button id="loginBtn">登录</button>
</body>
<script>
let createLoginLayer = function () {
let div = document.createElement('div');
div.className = 'dialog';
div.innerHTML = '我是登录浮窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
document.getElementById('loginBtn').onclick = function () {
let loginLayer = createLoginLayer();
loginLayer.style.display = 'flex';
};
</script>
</html>
这种方式每次点击按钮都会创建一个登录框
改进
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
56
57
58
59
60
61
62
63
64
65
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.dialog {
width: 200px;
height: 200px;
text-align: center;
line-height: 200px;
border: 1px solid red;
}
</style>
</head>
<body>
<button id="loginBtn">登录</button>
<button id="registerBtn">注册</button>
</body>
<script>
// 单例
function getSingle(fn) {
let result;
return function () {
if (!result) {
result = fn.apply(this, arguments)
}
return result;
}
}
// 创建登录弹窗
function createLoginDialog(text) {
let div = document.createElement('div');
div.innerHTML = text;
div.className = 'dialog';
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
// 创建注册弹窗
function createRegisterDialog(text) {
let div = document.createElement('div');
div.innerHTML = text;
div.className = 'dialog';
div.style.display = 'none';
document.body.appendChild(div);
return div;
}
let createSingleLoginDialog = getSingle(createLoginDialog);
let createSingleRegisterDialog = getSingle(createRegisterDialog);
document.getElementById('loginBtn').onclick = function () {
let loginLayer = createSingleLoginDialog('我是登录弹窗');
loginLayer.style.display = 'block';
};
document.getElementById('registerBtn').onclick = function () {
let loginLayer = createSingleRegisterDialog('我是注册弹窗');
loginLayer.style.display = 'block';
};
</script>
</html>
登录、注册复用了一套 单例 代码。
适用场景
“单例模式的特点,意图解决:维护一个全局实例对象。”
- 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
- 弹窗(登录框,信息提升框)
- 购物车 (一个用户只有一个购物车)
- 全局态管理 store (Vuex / Redux)
项目中引入第三方库时,重复多次加载库文件时,全局只会实例化一个库对象,如 jQuery,moment …, 其实它们的实现理念也是单例模式应用的一种:
1
2
3
4
5
6
// 引入代码库 libs(库别名)
if (window.libs != null) {
return window.libs; // 直接返回
} else {
window.libs = '...'; // 初始化
}