一、从「全局唯一」说起:单例模式的核心使命
1.1 为什么需要「单例」?
想象你有一个全局的存储管家,无论在哪个模块调用,都只能拿到同一个实例 —— 这就是单例模式的核心。在前端开发中,像 LocalStorage 封装、全局状态管理、日志服务等场景,都需要这种「唯一且全局可访问」的对象,避免重复实例化带来的资源浪费和数据混乱。
举个栗子🌰:如果每次操作 LocalStorage 都新建一个实例,就像每次打开冰箱都要重新买一台,显然不合理!单例模式就是让你拥有一台专属冰箱,随时取用。
1.2 ES6 类实现:优雅的静态控制法
class Storage {
static instance = null; // 静态属性保存唯一实例
constructor() {
if (Storage.instance) { // 防止重复实例化
throw new Error('请通过Storage.getInstance()获取实例~');
}
Storage.instance = this; // 首次实例化时保存自身
}
// 封装LocalStorage方法
getItem(key) { return localStorage.getItem(key); }
setItem(key, value) { localStorage.setItem(key, value); }
// 静态方法提供全局访问点
static getInstance() {
return Storage.instance || (Storage.instance = new Storage());
}
}
// 使用方式:全局唯一实例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2); // true,稳稳的唯一实例
关键点解析:
static instance作为类级别的存储容器,确保实例全局共享- 构造函数内的判空检查,从源头禁止「非法」实例化
getInstance静态方法实现「惰性初始化」,首次调用时才创建实例
1.3 ES5 闭包实现:复古但强大的作用域魔法
const Storage = (function() {
let instance = null; // 闭包内的私有变量,外部无法直接访问
function StorageBase() {
if (instance) { // 同样防止重复创建
return instance; // 直接返回已有实例
}
instance = this; // 首次调用时绑定实例
}
// 原型方法挂载,减少内存占用
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
StorageBase.prototype.setItem = function(key, value) {
localStorage.setItem(key, value);
};
// 返回工厂函数,控制实例创建
return function() {
if (!instance) {
instance = new StorageBase(); // 严格遵循单例规则
}
return instance;
};
})();
// 使用方式:通过调用函数获取实例
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2); // true,闭包确保实例唯一
闭包的关键作用:
instance变量被封闭在 IIFE(立即执行函数)作用域内,形成私有空间- 返回的工厂函数作为访问入口,保持对
instance的引用 - 兼容旧环境,无需 ES6 语法也能实现严格单例
二、闭包:单例背后的「作用域守护者」
2.1 闭包的本质:被记住的「私有空间」
简单来说,闭包就是「函数 + 其引用的外部变量」的组合。当内部函数被保存到外部,它会记住创建时的作用域链,让外部无法直接访问的变量得以「间接访问」。
用生活化的例子理解🍔:
闭包就像你点奶茶时的打包袋 —— 袋子(内部函数)不仅装着奶茶(函数逻辑),还带着你选的配料清单(外部变量),哪怕你把奶茶带回家,配料信息依然有效。
2.2 闭包如何助力单例?
在单例实现中,闭包主要解决两个问题:
- 私有变量保护:如 ES5 实现中的
instance,外部无法直接修改,只能通过工厂函数访问 - 状态持久化:确保多次调用时,
instance始终指向同一个实例,避免重复创建
2.3 闭包的双刃剑:优势与注意事项
| 优点 | 注意事项 |
|---|---|
| 封装性强,保护内部状态 | 过度使用可能导致内存泄漏(及时释放不再需要的引用) |
| 实现私有方法和属性 | 嵌套层级过深影响可读性(保持简洁设计) |
| 支持惰性初始化 | 避免滥用,非必要场景无需强制单例 |
三、进阶优化:让单例更「智能」
3.1 添加缓存机制:减少重复读取
// ES6版本优化,新增缓存功能
class Storage {
static instance = null;
constructor() {
if (Storage.instance) return Storage.instance;
Storage.instance = this;
this.cache = new Map(); // 使用Map存储缓存数据
}
getItem(key) {
if (this.cache.has(key)) { // 优先从缓存读取
return this.cache.get(key);
}
const value = localStorage.getItem(key);
this.cache.set(key, value); // 写入缓存
return value;
}
setItem(key, value) {
localStorage.setItem(key, value);
this.cache.set(key, value); // 同时更新缓存
}
}
优化价值:高频访问场景下减少 I/O 操作,提升性能,尤其适合移动端存储场景。
3.2 支持多种存储类型:localStorage/sessionStorage
// 增加存储类型配置,提升灵活性
class Storage {
static instance = null;
constructor(type = 'local') {
if (Storage.instance) return Storage.instance;
Storage.instance = this;
this.storage = type === 'local' ? localStorage : sessionStorage;
}
// 统一接口,内部根据type调用对应存储API
getItem(key) { return this.storage.getItem(key); }
setItem(key, value) { this.storage.setItem(key, value); }
}
// 使用示例:创建sessionStorage单例
const sessionStorage = Storage.getInstance('session');
适用场景:根据业务需求灵活切换存储类型,保持接口一致性。
四、避坑指南:单例模式的正确打开方式
4.1 避免构造函数污染
错误做法:
// ❌ 错误!未在构造函数中阻止重复实例化
class BadStorage {
static instance;
constructor() { /* 无判空逻辑 */ }
}
const s1 = new BadStorage(); // 允许直接new,破坏单例
const s2 = new BadStorage(); // s1 !== s2,单例失效!
正确做法:始终在构造函数中检查instance,必要时抛出明确错误。
4.2 区分「单例」与「全局变量」
单例模式≠全局变量,核心区别:
- 单例通过控制实例化过程保证唯一,支持惰性初始化
- 全局变量是简单的全局对象,无法阻止重复赋值或篡改
4.3 适配模块化环境
在 ES6 模块中,单例可以更简洁:
// storage.js
class Storage { /* 单例实现 */ }
export default Storage.getInstance(); // 直接导出实例,确保唯一
使用时直接导入,无需每次调用getInstance,保持代码简洁。
五、总结:单例与闭包的最佳拍档
单例模式就像一个「全局管家」,确保核心资源唯一可控;闭包则是这位管家的「秘密仓库」,用作用域魔法保护关键状态。两者结合,既能实现优雅的封装,又能兼顾性能与灵活性。
下次遇到「全局唯一」需求时,记得:
-
ES6 场景优先用
class + static实现,代码清晰易维护 -
兼容场景使用闭包 + IIFE,复古但可靠
-
复杂场景添加缓存、多类型支持等扩展功能,让单例更智能
掌握这对组合拳,无论是面试还是实战,都能轻松应对~ 代码的世界里,简洁与优雅永远是终极追求,而单例和闭包,正是通往这个目标的重要铺路石!