"如何设计一个全局唯一的本地存储工具类?" 这个问题看似简单,实则暗藏玄机 ------ 它考察的是单例模式(Singleton Pattern)的应用。单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。
为什么需要单例模式?从存储工具说起
在前端开发中,我们经常需要操作本地存储(LocalStorage)。但如果在多个地方分别创建存储实例,可能会导致以下问题:
- 资源浪费:每次创建实例都会占用额外的内存空间;
- 数据不一致:不同实例操作同一份数据时,可能因为状态不同步导致数据冲突;
- 难以维护:多个实例分散在代码中,增加了维护成本。
而单例模式可以完美解决这些问题:确保一个类只有一个实例,并提供全局访问点。比如,一个应用中应该只有一个 LocalStorage 的操作实例,所有地方都通过这个实例来读写数据。
单例模式的核心:唯一实例与全局访问
单例模式的实现有两个关键点:
- 唯一实例:类的构造函数需要被限制,不能随意创建新实例;
- 全局访问:提供一个静态方法或属性,让外界可以获取这个唯一实例。
javascript
function Storage() {
this.data = {};
// 实例方法:存储数据
this.setItem = function(key, value) {
this.data[key] = value;
};
// 实例方法:获取数据
this.getItem = function(key) {
return this.data[key];
};
}
// 通过new创建两个实例
const storage1 = new Storage();
const storage2 = new Storage();
console.log(storage1 === storage2, '~~~');
在 JavaScript 中,实现单例模式有多种方式,下面我们通过实现一个单例的
Storage
类来逐一讲解。
(1)ES6 Class 实现:静态属性 + 静态方法
使用 ES6 的class
语法,结合静态属性和静态方法,可以简洁地实现单例模式:
javascript
class Storage {
// 静态属性:存储唯一实例
static instance = null;
// 私有构造函数(可选)
constructor() {
// 如果已存在实例,直接返回,阻止新实例创建
if (Storage.instance) {
return Storage.instance;
}
// 初始化逻辑
console.log('Storage实例初始化');
Storage.instance = this; // 将当前实例赋值给静态属性
}
// 静态方法:获取唯一实例
static getInstance() {
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
// 存储方法
setItem(key, value) {
localStorage.setItem(key, value);
}
getItem(key) {
return localStorage.getItem(key);
}
}
// 使用示例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
console.log(storage1 === storage2);
storage1.setItem('name', '张三');
console.log(storage2.getItem('name'));

关键点解析:
static instance
:类的静态属性,用于存储唯一实例;static getInstance()
:静态方法,负责创建或返回已存在的实例;- 构造函数中判断
instance
是否存在,避免重复创建实例。
(2)闭包实现:立即执行函数 + 自由变量
除了类的方式,还可以使用闭包(立即执行函数)来实现单例:
javascript
// 基类:定义存储方法
class StorageBase {
setItem(key, value) {
localStorage.setItem(key, value);
}
getItem(key) {
return localStorage.getItem(key);
}
}
// 单例包装
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);
storage1.setItem('age', '25');
console.log(storage2.getItem('age'));
闭包实现的本质:
- 立即执行函数创建了一个闭包环境;
instance
变量被闭包捕获,始终保持在内存中;- 每次调用
Storage()
时,返回的都是同一个instance
。
(3)单例模式的 "懒汉式" 与 "饿汉式"
根据实例创建的时机,单例模式可分为两种:
-
懒汉式(Lazy Initialization) :
- 实例在第一次被使用时创建(如前面的例子);
- 优点:延迟加载,节省资源;
- 缺点:多线程环境下可能存在线程安全问题(JS 单线程,无需担心)。
-
饿汉式(Eager Initialization) :
- 实例在类加载时就创建;
- 优点:线程安全,无需判断;
- 缺点:提前占用资源。
下面是饿汉式的实现示例:
javascript
class Storage {
// 类加载时就创建实例
static instance = new Storage();
private constructor() {} // 私有构造函数
static getInstance() {
return Storage.instance;
}
// 存储方法...
}
实战:单例模式实现全局唯一登录弹窗
单例模式的应用远不止存储工具,登录弹窗也是典型场景 ------ 无论从哪个入口(导航栏、按钮、评论区)触发登录,都应该显示同一个弹窗实例。
需求分析
- 页面中多个按钮可打开登录弹窗;
- 弹窗全局唯一,避免重复创建 DOM;
- 支持显示 / 隐藏操作,状态统一。
完整实现代码
html
<body>
<button id="open">打开弹窗</button>
<button id="close">关闭弹窗</button>
<button id="open2">打开天窗</button>
<script>
// 单例模式实现弹窗
const Modal = (function () {
let modal = null;
return function () {
if (!modal) { // 第一次创建实例
modal = document.createElement('div');
modal.id = 'modal';
modal.innerHTML = `
<span class="close-btn">×</span>
<div>我是一个全局唯一的Modal</div>
`;
modal.style.display = 'none';
document.body.appendChild(modal);
// 绑定弹窗内部关闭按钮事件
modal.querySelector('.close-btn').addEventListener('click', function() {
modal.style.display = 'none';
});
}
return modal;
}
})();
// 绑定外部按钮事件
document.getElementById('open').addEventListener('click', function () {
const modal = new Modal();
modal.style.display = 'block';
});
document.getElementById('open2').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';
});
</script>
</body>
效果如下,当点击打开弹窗会显示弹窗,打开天窗还是保持原样,点击关闭,弹窗被隐藏
这个实现的核心是:
- 单例模式确保全局只有一个弹窗实例
- 延迟创建DOM,首次使用时才初始化
- 简单直接的显示 / 隐藏控制,没有复杂的交互逻辑
单例模式的优缺点:何时该用它?
(优点)
- 全局唯一性:确保整个应用中只有一个实例,便于数据共享和状态管理;
- 节约资源:避免重复创建实例,减少内存占用;
- 易于维护:全局访问点统一,修改时只需在一处改动。
(缺点)
- 违反单一职责原则:单例类负责创建自己的实例,又提供业务方法,职责过重;
- 扩展性差:单例类的结构通常比较固定,难以扩展;
- 单元测试困难:由于全局唯一,测试时可能影响其他测试用例。
单例模式的典型应用场景
单例模式在实际开发中非常常见,以下是一些典型场景:
- 配置管理:全局配置对象,确保所有地方使用相同的配置;
- 数据库连接:避免重复连接数据库,节省资源;
- 日志记录:统一的日志记录器,确保日志输出的一致性;
- 缓存系统:如 LocalStorage、SessionStorage 的封装,避免重复实例化;
- 全局状态管理:如 Redux 的 store,Vuex 的 store 等。
面试中如何优雅地实现单例模式?
面试时,除了实现基本功能,还可以考虑以下几点来展示你的专业性:
- 私有构造函数 :用
private
修饰构造函数(TypeScript 中),防止外部直接实例化; - 线程安全:虽然 JS 是单线程,但可以提一下多线程环境下的解决方案(如双重检查锁定);
- 扩展性:讨论如何在单例基础上支持扩展(如工厂模式结合单例);
- 内存管理:单例实例通常不会被垃圾回收,需注意内存占用。