JavaScript 单例模式
什么是单例模式?
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在JavaScript中,单例模式特别有用,因为它可以帮助我们管理应用程序的状态、协调系统操作并控制资源访问。
核心思想
无论创建多少次,单例模式总是返回同一个实例。
为什么需要单例模式?
在开发中,有些对象我们只需要一个实例:
- 管理应用程序的配置信息
- 管理共享资源(如数据库连接)
- 控制对某些资源的并发访问
- 协调系统中的行为
如果创建多个实例,可能会导致不必要的内存占用或产生逻辑错误。
单例模式的基本实现
下面是JavaScript中实现单例模式的最简方法:
javascript
const Singleton = (function() {
let instance;
function createInstance() {
// 这里可以是任何对象或值
return {
name: "我是单例对象",
printName: function() {
console.log(this.name);
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用单例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // 输出: true
instance1.printName(); // 输出: 我是单例对象
在这个实现中:
- 我们使用了立即执行函数(IIFE)创建一个闭包
instance
变量保存在闭包中,外部无法直接访问getInstance()
方法检查实例是否已经创建,如果没有才创建新实例
ES6类实现单例模式
使用ES6的类语法,我们可以这样实现单例模式:
javascript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
// 实例属性初始化
this.name = "ES6单例";
this.data = [];
// 保存实例
Singleton.instance = this;
}
// 实例方法
addData(item) {
this.data.push(item);
}
getData() {
return this.data;
}
}
// 使用
const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // 输出: true
s1.addData('测试数据');
console.log(s2.getData()); // 输出: ['测试数据']
单例模式的延迟初始化
有时我们不希望在程序启动时就创建单例对象,而是在第一次需要时才创建。这称为"延迟初始化"(Lazy Initialization):
javascript
class LazySingleton {
constructor() {
// 构造函数私有
}
static getInstance() {
if (!this.instance) {
this.instance = new LazySingleton();
}
return this.instance;
}
showMessage() {
console.log("我是延迟初始化的单例");
}
}
// 使用
const instance1 = LazySingleton.getInstance();
instance1.showMessage(); // 输出: 我是延迟初始化的单例
实际应用场景
1. 配置管理
单例模式非常适合管理应用程序的配置信息:
javascript
class ConfigManager {
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this.config = {
apiUrl: 'https://api.example.com',
timeout: 3000,
debug: false
};
ConfigManager.instance = this;
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
}
}
// 使用
const config = new ConfigManager();
console.log(config.get('apiUrl')); // 输出: https://api.example.com
2. 模态框管理
在网页应用中,通常只需要一个模态框组件,不同内容可以动态加载:
javascript
class ModalManager {
constructor() {
if (ModalManager.instance) {
return ModalManager.instance;
}
this.modal = null;
this.createModalElement();
ModalManager.instance = this;
}
createModalElement() {
// 创建模态框DOM元素
this.modal = document.createElement('div');
this.modal.className = 'modal';
this.modal.innerHTML = `
<div class="modal-content">
<span class="close">×</span>
<div class="modal-body"></div>
</div>
`;
document.body.appendChild(this.modal);
// 添加关闭按钮事件
this.modal.querySelector('.close').addEventListener('click', () => {
this.hide();
});
}
show(content) {
this.modal.querySelector('.modal-body').innerHTML = content;
this.modal.style.display = 'block';
}
hide() {
this.modal.style.display = 'none';
}
}
// 使用
const modalManager = new ModalManager();
document.getElementById('showModal').addEventListener('click', () => {
modalManager.show('<p>这是模态框内容</p>');
});
3. 数据存储
管理应用程序中的状态或共享数据:
javascript
class Store {
constructor() {
if (Store.instance) {
return Store.instance;
}
this._data = {};
Store.instance = this;
}
set(key, value) {
this._data[key] = value;
}
get(key) {
return this._data[key];
}
clear() {
this._data = {};
}
}
// 使用
const store = new Store();
store.set('currentUser', { id: 1, name: 'Alice' });
// 在应用程序的其他地方
const anotherStore = new Store();
console.log(anotherStore.get('currentUser')); // 输出: { id: 1, name: 'Alice' }
单例模式的优缺点
优点
- 节约资源:只创建一个实例,减少内存占用
- 方便协调行为:提供全局访问点,便于协调不同部分的交互
- 保证一致性:确保状态在应用程序中保持一致
缺点
- 全局状态:引入了全局状态,可能导致代码耦合
- 测试困难:全局状态使单元测试变得复杂
- 隐藏依赖:依赖单例的代码可能隐藏依赖关系
谨慎使用
不要过度使用单例模式。对于真正需要全应用共享的功能,单例是合适的;但对大多数情况,依赖注入可能是更好的选择。
单例模式与模块模式
在JavaScript中,模块模式(Module Pattern)和单例模式有很多相似之处,因为它们都利用闭包创建私有状态。事实上,使用模块化系统(如ES6模块)时,导出对象本身就具有单例特性:
javascript
// singleton.js
const instance = {
data: [],
add(item) {
this.data.push(item);
},
get() {
return this.data;
}
};
export default instance;
由于ES模块是单例的,无论在多少个地方导入这个模块,都会得到同一个实例:
javascript
// file1.js
import singleton from './singleton.js';
singleton.add('来自file1');
// file2.js
import singleton from './singleton.js';
console.log(singleton.get()); // 输出: ['来自file1']
总结
单例模式是一种简单而强大的设计模式,适用于需要全局协调的场景。在JavaScript中有多种方式实现单例,从传统的构造函数和原型模式到现代的ES6类和模块系统。虽然单例模式非常有用,但也应谨慎使用,避免创建过多的全局状态。
练习
- 创建一个简单的主题管理器(ThemeManager)单例,允许切换应用的主题(light/dark)
- 实现一个记录用户操作的日志管理器(LogManager)单例
- 使用单例模式创建一个购物车(ShoppingCart),确保整个应用只有一个购物车实例
额外资源
- 《JavaScript设计模式与开发实践》 - 曾探著
- 《Learning JavaScript Design Patterns》 - Addy Osmani
- 设计模式:可复用面向对象软件的基础
单例模式是最简单但也是最容易误用的设计模式之一。掌握它的精髓,将帮助你在合适的场景下构建更优雅的JavaScript应用程序。