单例模式
首发于:2022-03-22
基本概念
单例模式 (Singleton Pattern)又称为单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。
现实生活中的例子
现实中单例的例子很多,比如:
国家主席:选一个国家主席,已经选出来了,就不能再选了。
领结婚证:没结婚可以领结婚证,结婚了就不能再领了。
应用场景
单例模式的应用场景通常具有下面两种特点:
全局有且只能有一个这样的对象,像一些公共对象,需要用单例来保证访问的一致性,前端比较有代表性的就是
window/document
对象,模态弹窗等;实例化消耗资源比较多,可以使用单例模式来避免性能浪费,比如数据库连接、配置文件缓存等;
优缺点
单例模式主要解决的问题就是节约资源,保持访问一致性。
优点:
- 单例模式在创建后在内存中只存在一个实例,节约了内存开支和实例化时的性能开支,特别是需要重复使用一个创建开销比较大的类时,比起实例不断地销毁和重新实例化,单例能节约更多资源,比如数据库连接;
- 单例模式可以解决对资源的多重占用,比如写文件操作时,因为只有一个实例,可以避免对一个文件进行同时操作;
- 只使用一个实例,也可以减小垃圾回收机制 GC(Garbage Collecation) 的压力,表现在浏览器中就是系统卡顿减少,操作更流畅,CPU 资源占用更少;
缺点:
- 单例模式对扩展不友好,一般不容易扩展,因为单例模式一般自行实例化,没有接口;
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;
实现
常见简单实现
js
function ManageGame() {
if (ManageGame._schedule) { // 判断是否已经有单例了
return ManageGame._schedule
}
ManageGame._schedule = this
}
ManageGame.getInstance = function() {
if (ManageGame._schedule) { // 判断是否已经有单例了
return ManageGame._schedule
}
return ManageGame._schedule = new ManageGame()
}
const schedule1 = new ManageGame()
const schedule2 = ManageGame.getInstance()
console.log(schedule1 === schedule2) // true
class 语法实现
js
class ManageGame {
static _schedule = null
static getInstance() {
if (ManageGame._schedule) { // 判断是否已经有单例了
return ManageGame._schedule
}
return ManageGame._schedule = new ManageGame()
}
constructor() {
if (ManageGame._schedule) { // 判断是否已经有单例了
return ManageGame._schedule
}
ManageGame._schedule = this
}
}
const schedule1 = new ManageGame()
const schedule2 = ManageGame.getInstance()
console.log(schedule1 === schedule2) // true
上述方式实现的单例有个缺点就是都会暴露实例本身,外部可以直接修改。
通用实现
js
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);
这种实现方式用闭包对内部实例进行的了保护,外部代码无法修改,但是代价就是闭包会造成额外的开销,以及代码的复杂度增加,可读性降低。
利用块级作用域实现:
js
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
这是用块级作用域的方式来隐藏内部变量。
单例赋能
前面的例子中,我们在使用单例模式的时候每次都要写一大堆,而且里面可能还有这个类的一些初始化的业务逻辑,这并不符合单一职责原则。所以我们需要将单例模式的创建逻辑和特定类的业务逻辑拆开,这样功能逻辑就可以和正常的类一样。
js
/* 功能类 */
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
上面的代码使用 Proxy
来拦截默认的 new
方式,可以简化上面的代码为:
js
/* Person 类 */
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
/* 单例模式的赋能方法 */
function Singleton(FuncClass) {
let _instance
return new Proxy(FuncClass, {
construct(target, args) {
// Reflect.construct(FuncClass, args) 等同于 new FuncClass(...args)
return _instance || (_instance = Reflect.construct(FuncClass, args))
}
})
}
const PersonInstance = Singleton(Person)
const person1 = new PersonInstance('张小帅', 25)
const person2 = new PersonInstance('李小美', 23)
console.log(person1 === person2) // true
懒汉式与饿汉式
有时候一个实例化过程比较耗费性能的类,但是却一直用不到,如果一开始就对这个类进行实例化就显得有些浪费,那么这时我们就可以使用惰性创建,即延迟创建该类的单例。
懒汉式又被称为惰性单例,与饿汉式是相对的:
- 懒汉式单例是在使用时才实例化
- 饿汉式是当程序启动时或单例模式类一加载的时候就被创建
js
class FuncClass {
constructor() { this.bar = 'bar' }
}
// 饿汉式
const HungrySingleton = (function() {
const _instance = new FuncClass()
return function() {
return _instance
}
})()
// 懒汉式
const LazySingleton = (function() {
let _instance = null
return function() {
return _instance || (_instance = new FuncClass())
}
})()
const visitor1 = new HungrySingleton()
const visitor2 = new HungrySingleton()
const visitor3 = new LazySingleton()
const visitor4 = new LazySingleton()
console.log(visitor1 === visitor2) // true
console.log(visitor3 === visitor4) // true