单例模式是最简单的一种设计模式。同时也是使用最多的设计模式。我们在无意识中可能会大量使用它。

顾名思义,单例模式强调的就是一个类只有一个实例

INFO

JavaScript 中去思考单例,要比在其他开发语言里思考单例要简单很多。因为 JavaScript 本身就是单线程语言,不需要考虑多线程的情况下的同步锁问题。

例如,我们随便使用对象字面量创建一个对象,这就是最简单的单例。

code.ts
1
const p1 = {
2
token: '1xd33xsk21',
3
time: 20201219
4
}

在实践中,某些场景只需创建一次实例,而不用反复创建实例。

例如,单页应用中的登陆弹窗组件。该组件可能会出现多次,但我们完全没有必要在他每次出现时都创建一个实例。而只需在项目中创建一个实例就可以满足要求。

那么我们如何才能确保该登陆组件只会创建一个实例呢?

先按照正常的方式创建一个对象

code.ts
1
2
class Login {
3
constructor() {}
4
show() {}
5
hide() {}
6
// 渲染 DOM 节点
7
render() {}
8
}

当前的写法,当我们多次使用 new 关键时,就会创建多个实例,还不符合单例模式的要求。那么我们需要思考的问题,就是如何在多次使用 new 的时候,不会重复创建实例呢?

我们知道,创建实例的关键就在于构造函数,因此,只需要在构造函数中做一个重复的判断,就可以达到要求。在一个可以在内存中持久存在的地方添加一个引用,用于存储已经创建好的实例,在构造函数中判断,如果该引用已经存在了一个实例,那么就不再返回新的实例。

可以使用静态属性来在内存中存储已创建好的实例

code.ts
1
class Login {
2
// 使用静态属性在内存存储实例
3
static instance = null
4
constructor(parentNode) {
5
// 判断,如果已经存在实例,直接返回该实例
6
if (Login.instance) {
7
return Login.instance
8
}
9
this.parentNode = parentNode
10
this.render()
11
Login.instance = this
12
return this
13
}
14
15
show() { }
16
hide() { }
17
// 渲染 DOM 节点
18
render() { }
19
}
20
21
const p2 = new Login()
22
const p1 = new Login()
23
24
console.log(p1 === p2) // true

当然,也可以使用闭包在内存存储实例,只是写法不一样,思路与上面的方案无差别

code.ts
1
const Login = (function () {
2
// 使用 闭包 在内存存储实例
3
let instance = null
4
class LoginComponent {
5
constructor(parentNode) {
6
// 判断,如果已经存在实例,直接返回该实例
7
if (instance) {
8
return instance
9
}
10
this.parentNode = parentNode
11
this.render()
12
instance = this
13
return this
14
}
15
16
show() { }
17
hide() { }
18
// 渲染 DOM 节点
19
render() { }
20
}
21
return LoginComponent
22
})()
23
24
const p2 = new Login()
25
const p1 = new Login()
26
27
console.log(p1 === p2) // true

仔细观察用闭包存储实例,我们使用函数自执行的方式构建了闭包环境,如果我们换一种方式,假设我们的开发环境已经支持了模块化「import方式」,那么我们就可以将这种方式进行一个简单的调整

首先定义

code.ts
1
// components/login.js
2
let instance = null
3
class LoginComponent {
4
constructor(parentNode) {
5
// 判断,如果已经存在实例,直接返回该实例
6
if (instance) {
7
return instance
8
}
9
this.parentNode = parentNode
10
this.render()
11
instance = this
12
return this
13
}
14
15
show() { }
16
hide() { }
17
// 渲染 DOM 节点
18
render() { }
19
}
20
21
export default LoginComponent

在其他模块中使用

code.ts
1
import Login from 'components/login'
2
3
const p1 = new Login()
4
const p2 = new Login()
5
6
console.log(p1 === p2) // true

基于这个例子,大家可以体会一下:模块、闭包、单例 之间的关系。

进一步思考,如果我想要把已经封装好的类,转化为单例模式应该怎么办?前提是我期望之前的代码不会更改,或者是第三方依赖无法更改。

此时我们可以借助代理的思维方式来处理。

code.ts
1
class Login {
2
constructor() { }
3
show() { }
4
hide() { }
5
// 渲染 DOM 节点
6
render() { }
7
}
8
9
const LoginProxy = (function () {
10
let instance = null
11
return function() {
12
if (!instance) {
13
instance = new Login()
14
}
15
return instance
16
}
17
})()
18
19
const p1 = new LoginProxy()
20
const p2 = new LoginProxy()
21
22
console.log(p1 === p2)

我们可以将这种方式进行一个简单的调整,让其扩展性变得更强。

核心的思想是创建一个单例创建方法 singleCreator。

singleCreator 借助高阶函数的思想,用于扩展构造函数的逻辑。

code.ts
1
class Login {
2
constructor() { }
3
show() { }
4
hide() { }
5
// 渲染 DOM 节点
6
render() { }
7
}
8
9
// 该方法可以将其他的任何类改造成为单例模式
10
function singleCreator(constructor) {
11
let instance = null
12
return function() {
13
if (!instance) {
14
instance = new constructor()
15
}
16
return instance
17
}
18
}
19
20
const _Login = singleCreator(Login)
21
22
const p1 = new _Login()
23
const p2 = new _Login()
24
console.log(p1)
25
console.log(p1 === p2)
专栏首页
到顶
专栏目录