一、开篇:为什么单例模式是前端 er 的好朋友?🧩
在前端开发中,设计模式就像一套 "代码套路",帮我们解决重复出现的问题。而单例模式,堪称其中的 "专一王者"—— 它能保证一个类一辈子只实例化一次,并且提供一个全局的 "入口" 让我们随时访问这个唯一实例。
比如:全局的本地存储管理、页面中唯一的登录弹窗、全局状态管理器…… 这些场景都需要 "独一份" 的实例,单例模式就是为这类需求量身定做的!💡
二、单例模式:核心特点大揭秘 🔑
单例模式的核心可以总结为两句话:
- 唯一实例:不管调用多少次实例化方法,始终只有一个实例对象(就像 "世界上只有一个月亮"🌙);
- 全局访问:有一个统一的方法可以获取这个实例(就像一把万能钥匙,随时能打开那扇门)。
为什么要这么设计?
- 节省资源:避免重复创建实例(比如重复创建 DOM 元素、重复连接数据库);
- 统一管理:全局只有一个实例,数据和方法更易同步(比如本地存储的数据,任何地方访问都是一致的)。
三、本地存储单例:两种实现方式手把手教学 📦
本地存储(localStorage)是前端常用的存储方案,用单例模式封装它,能避免重复实例化带来的资源浪费。下面两种实现方式,详细到每一行代码都给你讲清楚!
方式 1:ES6 Class + 静态方法 🏗️
用 ES6 的 class 语法,通过静态属性和静态方法实现单例,逻辑清晰,一看就懂~
核心思路:
- 用
static instance保存唯一实例(相当于一个 "存档位"); - 用
static getInstance()方法控制实例创建:如果 "存档位" 为空,就新创建一个实例;否则直接返回已有的实例。
代码拆解:
class Storage {
// 静态属性:保存唯一实例(初始为空)
static instance;
// 构造函数:实例化时执行(仅第一次调用)
constructor() {
console.log("我是实例化时的日志~");
}
// 实例方法:存数据到localStorage
setItem(key, value) {
localStorage.setItem(key, value);
}
// 实例方法:从localStorage取数据
getItem(key) {
return localStorage.getItem(key);
}
// 静态方法:获取唯一实例的"入口"
static getInstance() {
// 关键判断:如果instance为空,就新创建一个;否则直接返回
if (!this.instance) {
this.instance = new Storage();
}
return this.instance;
}
}
验证单例效果:
// 两次获取实例
const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();
// 打印结果:true(证明是同一个实例!)
console.log(storage1 === storage2);
// 用storage1存数据
storage1.setItem('name', '前端学习者');
// 用storage2取数据(能拿到!因为是同一个实例)
console.log(storage2.getItem('name')); // 输出:前端学习者
✨ 亮点:ES6 的 class 语法让代码更结构化,静态方法getInstance()直观地作为全局访问入口,新手也能快速理解~
方式 2:闭包 + 立即执行函数 🔄
闭包是 JavaScript 的 "黑魔法",用它实现单例,能更灵活地封装实例,隐藏内部细节。
核心思路:
- 用 "立即执行函数" 创建一个独立作用域(相当于一个 "密室");
- 在作用域内定义
instance变量保存实例(只有闭包能访问); - 返回一个函数作为 "入口",每次调用时判断
instance是否存在,决定是否创建新实例。
代码拆解:
// 1. 定义基础构造函数(负责实现存储功能)
function StorageBase() {}
// 2. 给原型添加存储方法(所有实例共享)
StorageBase.prototype.setItem = function(key, value) {
localStorage.setItem(key, value);
};
StorageBase.prototype.getItem = function(key) {
return localStorage.getItem(key);
};
// 3. 用闭包封装单例逻辑
const Storage = (function() {
let instance = null; // 闭包内的变量,外部无法直接访问
return function() { // 返回一个函数作为"入口"
if (!instance) { // 第一次调用时创建实例
instance = new StorageBase();
}
return instance; // 后续调用直接返回已有实例
};
})(); // 立即执行函数,直接生成Storage"入口"
验证单例效果:
const storage1 = new Storage();
const storage2 = new Storage();
// 打印结果:true(还是同一个实例!)
console.log(storage1 === storage2);
storage1.setItem('age', '18');
console.log(storage2.getItem('age')); // 输出:18
🤫 亮点:闭包把instance藏得严严实实,外部无法修改,更符合 "封装" 的设计原则,适合需要隐藏细节的场景~
四、登录弹窗单例:性能与体验双提升 🚪
登录弹窗是单例模式的经典应用!想象一下:如果用户多次点击 "登录" 按钮,每次都创建一个新弹窗,不仅 DOM 会乱套,还会浪费性能。用单例模式,让弹窗 "一生只出现一次"~
核心需求:
- 弹窗全局唯一,无论点击多少次 "打开",都只创建一个 DOM 元素;
- 初始不加载,等用户第一次点击时再创建(90% 用户可能不登录,提前加载纯纯浪费!⏳);
- 支持 "打开 / 关闭" 操作,状态统一。
代码实现:
<!-- 按钮区域 -->
<button id="open">打开弹窗</button>
<button id="close">关闭弹窗</button>
<button id="open2">再打开一次</button>
<script>
// 用闭包实现弹窗单例
const Model = (function() {
let model = null; // 保存唯一弹窗实例
return function() {
if (!model) { // 第一次调用时创建弹窗
// 1. 创建DOM元素
model = document.createElement('div');
// 2. 设置内容和样式
model.innerHTML = '我是唯一的登录弹窗~';
model.id = 'model';
model.style = `
height: 200px; width: 200px; line-height: 200px;
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
border: 1px solid #000; background: #fff; text-align: center;
display: none; /* 初始隐藏 */
`;
// 3. 添加到页面
document.body.appendChild(model);
}
return model; // 后续调用直接返回已有弹窗
};
})();
// 绑定按钮事件
document.getElementById('open').addEventListener('click', () => {
const model = new Model();
model.style.display = 'block'; // 显示弹窗
});
document.getElementById('close').addEventListener('click', () => {
const model = new Model();
model.style.display = 'none'; // 隐藏弹窗
});
document.getElementById('open2').addEventListener('click', () => {
const model = new Model();
model.style.display = 'block'; // 再次显示(还是同一个弹窗!)
});
</script>
为什么这么好?
- 性能:只创建一次 DOM,避免重复渲染(🚀 速度 up up);
- 体验:无论点击哪个 "打开" 按钮,都是同一个弹窗,状态不会乱;
- 灵活性:延迟到第一次使用时创建,节省初始加载时间。
五、总结:单例模式的 "三板斧" 🌟
-
唯一实例:通过静态属性或闭包保存实例,确保只创建一次;
-
全局访问:提供统一入口(如
getInstance()或闭包返回的函数); -
场景适配:适合全局资源(存储、弹窗、状态管理等),提升性能和可维护性。
下次遇到 "需要一个全局唯一实例" 的场景,就请出单例模式吧~ 它会像一个靠谱的管家,帮你把代码打理得井井有条!💼