JavaScript 单例模式实践:用 Class 和闭包封装 Storage
单例模式是 JavaScript 中最常用的设计模式之一,它确保一个类只有一个实例,并提供一个全局访问点。本文将详细介绍如何使用 ES6 Class 和闭包两种方式实现单例模式,并封装一个基于 localStorage 的 Storage 工具。
什么是单例模式?
单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。这在需要全局状态管理或避免重复创建相同功能的对象时非常有用。
单例模式的特点:
- 一个类只有一个实例
- 必须自行创建这个实例
- 必须向整个系统提供这个实例
在前端开发中,单例模式常用于:
- 全局状态管理
- 缓存系统
- 对话框、弹窗等UI组件
- 与浏览器API(如localStorage)交互的工具类
使用 ES6 Class 实现 Storage 单例
让我们先用 ES6 Class 实现一个基于 localStorage 的单例 Storage 工具:
javascript
class Storage {
// 静态属性,用于存储唯一实例
static instance = null;
// 私有构造函数,防止外部直接实例化
constructor() {
if (!Storage.instance) {
// 初始化操作
this.store = window.localStorage;
Storage.instance = this;
}
return Storage.instance;
}
// 静态方法,用于获取单例实例
static getInstance() {
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
// 封装 localStorage 的 setItem 方法
setItem(key, value) {
try {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
this.store.setItem(key, value);
} catch (e) {
console.error('Storage setItem error:', e);
}
}
// 封装 localStorage 的 getItem 方法
getItem(key) {
try {
let value = this.store.getItem(key);
try {
value = JSON.parse(value);
} catch (e) {
// 如果不是 JSON 字符串,直接返回原值
}
return value;
} catch (e) {
console.error('Storage getItem error:', e);
return null;
}
}
// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true,确实是同一个实例
storage1.setItem('name', '张三');
console.log(storage2.getItem('name')); // '张三'
代码解析
- 静态属性
instance:用于存储类的唯一实例,初始为 null。 - 构造函数:检查是否已存在实例,如果不存在则创建并存储到
instance中。这样确保无论调用多少次new Storage(),都返回同一个实例。 - 静态方法
getInstance():提供获取单例的标准方法,这是单例模式的典型实现方式。 - 封装 localStorage 方法:对原生 localStorage 的方法进行封装,增加了错误处理和 JSON 自动转换功能。
- 使用示例:展示了如何获取实例并验证单例特性,以及基本的使用方法。
使用闭包实现 Storage 单例
对于更喜欢函数式编程的开发者,可以使用闭包来实现同样的功能:
javascript
const Storage = (function() {
let instance = null;
let store = null;
function init() {
// 私有方法
function setItem(key, value) {
try {
if (typeof value === 'object') {
value = JSON.stringify(value);
}
store.setItem(key, value);
} catch (e) {
console.error('Storage setItem error:', e);
}
}
function getItem(key) {
try {
let value = store.getItem(key);
try {
value = JSON.parse(value);
} catch (e) {
// 如果不是 JSON 字符串,直接返回原值
}
return value;
} catch (e) {
console.error('Storage getItem error:', e);
return null;
}
}
function removeItem(key) {
store.removeItem(key);
}
return {
setItem,
getItem,
removeItem
};
}
return {
getInstance: function() {
if (!instance) {
store = window.localStorage;
instance = init();
}
return instance;
}
};
})();
// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true
storage1.setItem('age', 25);
console.log(storage2.getItem('age')); // 25
代码解析
- 立即执行函数 (IIFE) :创建一个闭包环境,保护私有变量
instance和store不被外部访问。 - init 函数:初始化真正的存储功能,包含所有私有方法。
- 返回对象:只暴露
getInstance方法,确保单例特性。 - 使用示例:与 Class 版本相同,验证单例特性和基本使用方法。
Class 与闭包实现的比较
| 特性 | Class 实现 | 闭包实现 |
|---|---|---|
| 可读性 | 更清晰,结构更直观 | 需要理解闭包概念 |
| 私有性 | 需要约定(如 _ 前缀) | 天然的私有性 |
| 扩展性 | 易于继承和扩展 | 较难扩展 |
| 内存效率 | 稍高(原型继承) | 稍低(每个方法都是新的) |
| 现代 JavaScript 特性 | 使用最新语法 | 使用传统模式 |
单例模式的优缺点
优点
- 严格控制实例数量:确保全局唯一实例,避免资源浪费。
- 全局访问点:方便在应用各处访问同一实例。
- 延迟初始化:只有在第一次使用时才创建实例。
缺点
- 全局状态:可能导致代码耦合度高,难以测试。
- 违反单一职责原则:单例类既管理自己的生命周期又负责业务逻辑。
- 内存泄漏风险:如果单例持有大量数据,可能不会及时释放。
实际应用中的注意事项
- 线程安全:在 JavaScript 单线程环境中不需要考虑,但在其他语言中需要注意。
- 序列化问题:如果需要序列化单例对象,需要考虑如何保持单例特性。
- 测试困难:单例的全局状态可能使单元测试变得复杂,可以考虑依赖注入。
- 过度使用:不要滥用单例模式,只有在真正需要唯一实例时才使用。
总结
单例模式是一种强大而常用的设计模式,特别适合管理全局状态和封装浏览器API。本文介绍了两种实现方式:
- ES6 Class 实现:更现代、更直观,适合大多数现代JavaScript项目。
- 闭包实现:更函数式,提供更好的封装性,适合喜欢函数式编程的开发者。
在封装 localStorage 时,我们不仅实现了单例模式,还增加了错误处理、JSON自动转换和过期时间等实用功能,使这个Storage工具更加健壮和实用。
记住,设计模式是工具而不是目标,选择最适合你项目需求和团队习惯的实现方式才是最重要的。单例模式虽好,但也要避免滥用,特别是在需要多实例或测试驱动开发的场景中。