前言
在日常开发中,我们经常遇到这样的场景:需要一个全局唯一的对象来管理某些资源,比如数据库连接池、缓存管理器,或者页面上的弹窗组件。这时候,单例模式就显得尤为重要了。
单例模式作为23种设计模式中的一种,它的核心思想很简单:确保一个类只有一个实例,并提供全局访问点。今天我们就来深入探讨一下在JavaScript中如何优雅地实现单例模式。
什么是单例模式?
单例模式是一种创建型设计模式,它保证一个类仅有一个实例,并提供一个访问它的全局访问点。这种模式在需要控制资源访问、避免重复创建对象的场景中非常有用。
单例模式的核心特征:
- 类只能有一个实例
- 必须自行创建这个实例
- 必须给其他对象提供这一实例
ES6 Class实现单例模式
让我们从一个实际的例子开始------实现一个基于localStorage的Storage类。
基础实现
js
class Storage {
static instance;
constructor() {
console.log(this, '~~~');
}
// 静态方法:获取单例实例
static getInstance() {
// 如果还没有实例化过,则创建新实例
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
localStorage.setItem(key, value);
}
}
// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true - 证明是同一个实例
storage1.setItem('name', '鹿老板');
console.log(storage1.getItem('name')); // 鹿老板
console.log(storage2.getItem('name')); // 鹿老板 - 同一个实例,数据共享
关键点解析
- static instance:静态属性,用于保存唯一的实例
- static getInstance() :静态方法,负责创建和返回实例
- 惰性实例化:只有在第一次调用时才创建实例,提高性能
这种实现方式的优势在于:
- 语法简洁,易于理解
- 利用ES6的static关键字,代码更加规范
- 性能优秀,避免了重复创建对象的开销
闭包实现单例模式
在ES6之前,我们通常使用闭包来实现单例模式。这种方式虽然看起来复杂一些,但理解了闭包的原理后,会发现它的设计非常巧妙。
js
// 基础构造函数
function StorageBase() {
// 构造函数体
}
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
StorageBase.prototype.setItem = function(key, value) {
localStorage.setItem(key, value);
};
// 使用闭包实现单例
const Storage = (function() {
let instance = null; // 自由变量,保存唯一实例
return function() {
if (!instance) {
instance = new StorageBase();
}
return instance;
};
})();
// 使用方式
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2); // true
storage1.setItem('name', 'V老板');
console.log(storage1.getItem('name')); // V老板
console.log(storage2.getItem('name')); // V老板
闭包实现的优势
- 作用域隔离:instance变量被封闭在闭包内,外部无法直接访问
- 兼容性好:不依赖ES6语法,在老版本浏览器中也能正常工作
- 灵活性强:可以在闭包内部添加更多的私有变量和方法
实战应用:Modal弹窗单例
理论讲完了,让我们来看一个更贴近实际开发的例子------登录弹窗的单例实现。
业务场景分析
在实际项目中,登录弹窗有以下特点:
- 全站只需要一个登录弹窗
- 90%的用户可能不会登录,不应该在页面加载时就创建DOM
- 需要支持多次打开/关闭,但始终是同一个DOM元素
代码实现
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modal 登入弹窗单例</title>
<style>
#modal {
position: fixed;
line-height: 200px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
border: 1px solid #000;
text-align: center;
background: white;
z-index: 1000;
}
</style>
</head>
<body>
<button id="open">打开弹窗</button>
<button id="close">关闭弹窗</button>
<button id="open2">打开弹窗2</button>
<script>
// 使用闭包实现Modal单例
const Modal = (function() {
let modal = null;
return function() {
if (!modal) { // 第一次和唯一一次创建
modal = document.createElement('div');
modal.innerHTML = '我是一个全局唯一的Modal';
modal.id = 'modal';
modal.style.display = 'none';
document.body.appendChild(modal);
}
return modal;
};
})();
// 事件绑定
document.getElementById('open').addEventListener('click', function() {
const modal = new Modal();
modal.style.display = 'block';
});
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal();
modal.style.display = 'none';
});
document.getElementById('open2').addEventListener('click', function() {
const modal = new Modal();
modal.style.display = 'block';
});
</script>
</body>
</html>

实现亮点
- 懒加载:DOM元素在第一次需要时才创建,避免了不必要的性能开销
- 资源复用:多个按钮操作的都是同一个DOM元素,节省内存
- 状态管理:通过display属性控制显示/隐藏,状态在实例间共享
单例模式的优缺点
优点
- 内存节省:确保只有一个实例存在,减少内存占用
- 全局访问:提供全局访问点,方便状态管理
- 延迟实例化:支持懒加载,按需创建实例
- 线程安全:在JavaScript单线程环境中,天然避免了多线程同步问题
缺点
- 测试困难:全局状态使得单元测试变得复杂
- 扩展性差:违反了开闭原则,不易扩展
- 隐式依赖:代码之间的依赖关系不够明确
适用场景
- 配置管理器
- 日志记录器
- 数据库连接池
- 缓存管理器
- 弹窗、Toast等UI组件
现代JavaScript中的替代方案
模块模式
js
// storage.js
class StorageManager {
constructor() {
this.cache = new Map();
}
getItem(key) {
return localStorage.getItem(key);
}
setItem(key, value) {
localStorage.setItem(key, value);
}
}
// 导出单例实例
export default new StorageManager();
使用Symbol确保唯一性
js
const INSTANCE = Symbol('instance');
class Storage {
static [INSTANCE] = null;
static getInstance() {
if (!Storage[INSTANCE]) {
Storage[INSTANCE] = new Storage();
}
return Storage[INSTANCE];
}
}
总结
单例模式作为一种经典的设计模式,在JavaScript开发中有着广泛的应用。通过本文的学习,我们了解了:
- ES6 Class实现:语法简洁,易于理解和维护
- 闭包实现:兼容性好,作用域隔离更彻底
- 实战应用:Modal弹窗展示了单例模式在UI组件中的价值
- 现代替代方案:模块模式和Symbol的使用
在实际开发中,我们应该根据具体场景选择合适的实现方式,既要考虑代码的可维护性,也要关注性能和用户体验。设计模式是工具,而不是目的,合理使用才能发挥其真正的价值。