最近在整理项目代码时发现,无论是全局状态管理还是工具类封装,单例模式简直无处不在!今天就结合一点实际代码,用大白话聊聊单例模式到底是个啥,以及怎么用JS实现它。
先问个问题:为什么需要单例模式?
假设你在开发一个电商网站的购物车功能,用户点击"加入购物车"时,总不能每次都创建一个新的购物车对象吧?那样数据不就乱套了!
单例模式解决的核心问题 :一个类只能创建「唯一实例」,不管你调用多少次,拿到的都是同一个对象。就像公司的CEO只有一个,全国的首都只有一个——保证全局唯一性。
一个类只有一个实例,并且给出一个访问它的方法: “性能特别好,好管理” ——这就是单例模式的精髓!
JS实现单例模式的两种实战方案
方案一:ES6 Class + 静态方法(推荐现代项目)
先看一段简单的代码(我简化并加了注释):
<!DOCTYPE html>
<html>
<body>
<script>
class Storage {
// 构造函数:初始化时打印一下(方便调试)
constructor() {
console.log('我被实例化了!');
}
// 静态属性:存唯一实例(static关键字让它属于类,不属于实例)
static instance;
// 静态方法:获取实例的唯一入口
static getInstance() {
// 如果实例不存在,就new一个;如果存在,直接返回
if (!Storage.instance) {
Storage.instance = new Storage();
}
return Storage.instance;
}
// 业务方法:存数据到localStorage
setItem(key, value) {
localStorage.setItem(key, value);
}
// 业务方法:取数据
getItem(key) {
return localStorage.getItem(key);
}
}
// 测试:两次调用getInstance
const cart1 = Storage.getInstance();
const cart2 = Storage.getInstance();
console.log(cart1 === cart2); // true!证明是同一个实例
</script>
</body>
</html>
核心逻辑 :
- 用
static instance存唯一实例(静态属性属于类本身,不会被实例继承) static getInstance()做「门卫」:第一次调用时创建实例,之后直接返回已有的- 业务方法(
setItem/getItem)写在原型上,所有实例共享
优点 :代码清晰,符合面向对象思维,ES6语法原生支持 注意 :别直接 new Storage() !一定要通过 getInstance() 获取实例,否则会绕过单例逻辑。
方案二:闭包 + 立即执行函数(兼容老项目)
如果你的项目还在用ES5语法, 2.html 的闭包方案更合适(这是JS特有的黑科技):
<!DOCTYPE html>
<html>
<body>
<script>
// 1. 先定义一个基础类(封装localStorage操作)
function StorageBase() {}
StorageBase.prototype.setItem = function(key, value) {
localStorage.setItem(key, value);
};
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
// 2. 用闭包创建单例工厂
const Storage = (function() {
let instance = null; // 闭包变量:藏在这里,外部访问不到
return function() {
if (!instance) {
instance = new StorageBase(); // 第一次调用时创建实例
}
return instance; // 后续调用直接返回已有实例
};
})();
// 测试:两次new Storage()
const cart1 = new Storage();
const cart2 = new Storage();
console.log(cart1 === cart2); // true!依然是同一个实例
// 实际使用
cart1.setItem('name', '掘金用户');
console.log(cart2.getItem('name')); // '掘金用户'(数据共享)
</script>
</body>
</html>
核心逻辑 :
- 用 IIFE(立即执行函数)创建一个「私有作用域」,把
instance藏在里面 - 返回一个新函数,每次调用时检查
instance是否存在 - 外部只能通过
new Storage()调用,但不管 new 多少次,拿到的都是同一个instance
优点 :兼容性好(ES5及以上都支持),真正做到了「无法直接创建实例」 缺点 :代码稍微绕一点,不如Class方案直观
单例模式在实战中的3个黄金场景
结合咱们实际的需求(封装localStorage),再扩展几个高频场景:
全局状态管理(如购物车、用户信息)
就像上面的例子,整个应用的购物车数据必须存在一个实例里,否则不同组件拿到的购物车数据可能不一致。
class ShoppingCart {
// 静态属性存储唯一实例
static instance;
// 私有构造函数:初始化购物车数据
constructor() {
// 从localStorage加载数据(刷新页面不丢失)
const savedCart = localStorage.getItem('cart');
this.items = savedCart ? JSON.parse(savedCart) : [];
}
// 静态方法:获取唯一实例
static getInstance() {
if (!ShoppingCart.instance) {
ShoppingCart.instance = new ShoppingCart();
}
return ShoppingCart.instance;
}
// 核心业务方法
// 1. 添加商品
addItem(product) {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += product.quantity;
} else {
this.items.push(product);
}
this._saveToLocalStorage();
}
// 2. 删除商品
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
this._saveToLocalStorage();
}
// 3. 获取购物车总数
getTotalCount() {
return this.items.reduce((total, item) => total + item.quantity, 0);
}
// 4. 获取购物车总价
getTotalPrice() {
return this.items.reduce((total, item) => total + (item.price * item.quantity), 0);
}
// 5. 清空购物车
clearCart() {
this.items = [];
this._saveToLocalStorage();
}
// 私有方法:保存数据到localStorage
_saveToLocalStorage() {
localStorage.setItem('cart', JSON.stringify(this.items));
}
}
// 测试代码(模拟不同页面调用)
const cart1 = ShoppingCart.getInstance();
const cart2 = ShoppingCart.getInstance();
// 验证单例唯一性
console.log(cart1 === cart2); // true
// 添加商品(模拟首页添加)
cart1.addItem({
id: 1,
name: '掘金周边-马克杯',
price: 59,
quantity: 1
});
// 再次添加(模拟详情页添加)
cart2.addItem({
id: 1,
name: '掘金周边-马克杯',
price: 59,
quantity: 1 // 此时购物车中该商品数量会变为2
});
// 模拟结算页获取数据
console.log('商品总数:', cart1.getTotalCount()); // 2
console.log('商品总价:', cart1.getTotalPrice()); // 118
工具类封装(如请求工具、日期格式化)
比如封装Axios请求:
class Http {
static instance;
static getInstance() { /* 单例逻辑 */ }
get(url) { /* 发送GET请求 */ }
}
// 全项目用同一个Http实例,统一配置baseURL、拦截器
弹窗组件(如全局提示框、登录弹窗)
页面上的「登录弹窗」永远只需要一个DOM节点,用单例模式控制弹窗的创建和销毁,避免重复渲染。
单例模式的注意事项
- 别随便改静态属性 :如果有人直接 Storage.instance = null ,单例就被破坏了(解决方案:用ES6的 # 私有属性,或Symbol)
- 谨慎用在多线程环境 :Node.js的cluster模式下可能会创建多个实例(前端一般不用操心)
- 别过度使用 :不是所有类都需要单例!像「用户对象」「商品对象」这种需要多实例的场景就别用。
总结
单例模式就像编程世界里的「独生子女政策」——保证一个类只有一个实例,从而实现数据共享、节省资源。
咱们项目里的两种实现方案各有千秋:
- Class方案 :适合现代前端项目(React/Vue),代码清晰
- 闭包方案 :适合需要兼容老环境的场景,封装性更强
总的来说,单例模式通过控制类的唯一实例,实现了资源高效利用与全局状态统一管理,是前端工程化中提升性能与可维护性的实用方案。