一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情。
单例模式描述
单例模式 (Singleton Pattern)又称为单体模式,顾名思义,单例模式中Class的实例个数最多为1,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。
当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。
单例模式可能是设计模式里面最简单的模式了,虽然简单,但在我们日常生活和编程中却经常接触到,来看一下实际遇到的单例模式的场景:
- 比如玩单机或者运营类游戏,第二天继续玩时,我们希望是拿到同一份存档接着之前的玩,而不是每次都是从头开始。
- 编程中也有很多对象我们只需要唯一一个,比如数据库连接、线程池、配置文件缓存、浏览器中的 window/document 等,如果创建多个实例,会带来资源耗费严重,或访问行为不一致等情况。
- 类似于数据库连接实例,我们可能频繁使用,但是创建它所需要的开销又比较大,这时只使用一个数据库连接就可以节约很多开销。一些文件的读取场景也类似,如果文件比较大,那么文件读取就是一个比较重的操作。比如这个文件是一个配置文件,那么完全可以将读取到的文件内容缓存一份,每次来读取的时候访问缓存即可,这样也可以达到节约开销的目的。
另外前端业务中可能遇到的单例模式:
- 定义命名空间和实现分支型方法
- 登录框/单点登录
vuex
和redux
中的store
在类似场景中,这些例子有以下特点:
- 每次访问者来访问,返回的都是同一个实例;
- 如果一开始实例没有创建,那么这个特定类需要自行创建这个实例;
实现单例模式
实现单例模式需要解决以下几个问题:
- 如何确定Class只有一个实例?
- 如何简便的访问Class的唯一实例?
- Class如何控制实例化的过程?
- 如何将Class的实例个数限制为1?
我们一般通过实现以下两点来解决上述问题:
- 隐藏Class的构造函数,避免多次实例化
- 通过暴露一个
getInstance()
方法来创建/获取唯一实例
根据上面游戏的例子提炼一下单例模式,游戏可以被认为是一个特定的类(Singleton),而存档是单例(instance),每次访问特定类的时候,都会拿到同一个实例。主要有下面几个概念:
Singleton
:特定类,这是我们需要访问的类,访问者要拿到的是它的实例;instance
:单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;getInstance
:获取单例的方法,或者直接由 new 操作符获取;
1、IIFE 方式创建单例模式
const Singleton = (function() {
let _instance = null // 存储单例
const Singleton = function() {
if (_instance) return _instance // 判断是否已有单例
_instance = this
this.init() // 初始化操作
return _instance
}
// 初始化方法
Singleton.prototype.init = function() {
this.foo = 'Singleton Pattern'
}
// 访问实例方法
Singleton.getInstance = function() {
if (_instance) return _instance
_instance = new Singleton()
return _instance
}
return Singleton
})()
const visitor1 = new Singleton()
const visitor2 = new Singleton() // 既可以 new 获取单例
const visitor3 = Singleton.getInstance() // 也可以 getInstance 获取单例
// 每次生成及访问都是同一个实例
console.log(visitor1 === visitor2) // true
console.log(visitor1 === visitor3) // true
- 闭包开销,并且因为 IIFE 操作带来了额外的复杂度,让可读性变差。
- IIFE 内部返回的
Singleton
才是我们真正需要的单例的构造函数
,外部的Singleton
把它和一些单例模式的创建逻辑进行了一些封装。 - IIFE 方式除了直接返回一个方法/类实例之外,还可以通过模块模式的方式来进行。
IIFE 方式本质还是通过函数作用域的方式来隐藏内部作用域的变量,有了 ES6 的 let/const
之后,可以通过 { }
块级作用域的方式来隐藏内部变量
2、块级作用域方式创建单例
let getInstance;
{
let _instance = null // 存储单例
// 实际单例的构造函数
const Singleton = function() {
if (_instance) return _instance // 判断是否已有单例
_instance = this
this.init() // 初始化操作
return _instance
}
// 初始化
Singleton.prototype.init = function() {
this.foo = 'Singleton Pattern'
}
// 访问/获取实例方法
getInstance = function() {
if (_instance) return _instance
_instance = new Singleton()
return _instance
}
}
const visitor1 = getInstance()
const visitor2 = getInstance()
console.log(visitor1 === visitor2) // true
3、单例模式赋能
之前的例子中,单例模式的创建逻辑和原先这个类的一些功能逻辑(比如 init
等操作)混杂在一起,根据单一职责原则,这个例子我们还可以继续改进一下,将单例模式的创建逻辑和特定类的功能逻辑拆开,这样功能逻辑就可以和正常的类一样。
/* 功能类 */
class FuncClass {
constructor(bar) {
this.bar = bar
this.init()
}
init() {
this.foo = 'Singleton Pattern'
}
}
/* 单例模式的赋能类 */
const Singleton = (function() {
let _instance = null // 存储单例
const ProxySingleton = function(bar) {
if (_instance) return _instance // 判断是否已有单例
_instance = new FuncClass(bar)
return _instance
}
ProxySingleton.getInstance = function(bar) {
if (_instance) return _instance
_instance = new Singleton(bar)
return _instance
}
return ProxySingleton
})()
const visitor1 = new Singleton('单例1')
const visitor2 = new Singleton('单例2')
const visitor3 = Singleton.getInstance()
console.log(visitor1 === visitor2) // true
console.log(visitor1 === visitor3) // true
总结
优点
- 划分命名空间,减少全局变量
- 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
- 且只会实例化一次。简化了代码的调试和维护
缺点
- 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合
- 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。
我们应该在什么场景下使用单例模式呢:
- 当一个类的实例化过程消耗的资源过多,可以使用单例模式来避免性能浪费;
- 当项目中需要一个公共的状态,那么需要使用单例模式来保证访问一致性;