前言
之前介绍了单例模式实现Storage:juejin.cn/post/752720… ,今天我们再来详细聊一下单例模式来实现弹窗功能。
在 Web 开发中,登录弹窗(Modal) 是一个非常常见的交互组件。为了提升用户体验和页面性能,我们希望:
- 不跳转页面,直接在当前页面弹出登录框;
- 不提前加载,只在用户点击“登录”时才创建;
- 只创建一次,避免重复渲染和资源浪费;
- 全局唯一,多个按钮都能访问到同一个弹窗实例。
这就非常适合使用 单例模式(Singleton Pattern) 来实现。
本文目标
我们将使用 三种方式 来实现登录弹窗的单例模式:
- 闭包 + IIFE(立即执行函数)
- 类(class) + 静态属性
- 模块模式(Module Pattern)
并通过一个统一的 HTML 页面来演示它们的使用和效果。
基础 HTML 结构(统一使用)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>登录弹窗单例实现</title>
<style>
#modal {
width: 300px;
height: 200px;
line-height: 200px;
text-align: center;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid #000;
background: #fff;
z-index: 999;
display: none;
}
#mask {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
background: rgba(0,0,0,0.5);
z-index: 998;
display: none;
}
</style>
</head>
<body>
<button id="open1">打开弹窗 - 方式一</button>
<button id="open2">打开弹窗 - 方式二</button>
<button id="open3">打开弹窗 - 方式三</button>
<button id="close">关闭弹窗</button>
<script>
// 我们将在这里分别实现三种方式
</script>
</body>
</html>
方法一:闭包 + IIFE(最简洁的单例实现)
🧩 实现逻辑:
- 使用 IIFE 创建一个私有作用域;
- 使用闭包保存
modal实例; - 只在第一次调用时创建弹窗;
- 后续调用返回同一个实例。
🧩 实现代码
const ModalSingleton1 = (function () {
let modal = null; // 闭包中的私有变量,用于存储唯一的弹窗实例
return function () {
if (!modal) { // 只有当 modal 为 null 时才创建新实例
modal = document.createElement('div');
modal.id = 'modal';
modal.innerHTML = '我是一个 Modal(方式一)';
document.body.appendChild(modal);
console.log('方式一:创建了弹窗'); // 第一次调用时打印日志
}
return modal; // 返回已存在的或新创建的唯一实例
};
})();
关键在于:通过闭包保留了
modal的引用,并且在首次调用时创建并返回该实例。后续的所有调用都会直接返回这个已创建的实例,从而实现了单例模式。
优点:
- 简洁、直观;
- 利用闭包实现私有变量;
- 不依赖类语法,兼容性好。
方法二:类(class)+静态属性(面向对象的写法)
🧩 实现逻辑:
- 使用
class定义弹窗类; - 使用静态属性
_instance保存实例; - 构造器中判断是否已存在实例;
- 如果存在则返回,否则创建。
🧩 实现代码:
class ModalSingleton2 {
static _instance = null; // 静态属性,用于存储唯一的实例
constructor() {
if (ModalSingleton2._instance) { // 如果已有实例,则直接返回该实例
return ModalSingleton2._instance;
}
this.modal = document.createElement('div');
this.modal.id = 'modal';
this.modal.innerHTML = '我是一个 Modal(方式二)';
document.body.appendChild(this.modal);
console.log('方式二:创建了弹窗'); // 第一次创建实例时打印日志
ModalSingleton2._instance = this; // 将当前实例赋值给静态属性
}
getModal() {
return this.modal; // 提供一个公共方法获取模态框实例
}
}
关键在于:使用 静态属性
_instance来保存唯一的实例,这样无论创建多少个对象实例,都指向同一个_instance。在构造器中检查_instance是否存在,如果存在则直接返回该实例,否则创建一个新的实例并赋值给_instance。
优点:
- 更符合现代 JS 的面向对象风格;
- 可扩展性强(可以加方法、状态);
- 逻辑清晰,适合大型项目。
方法三:模块模式—— 模拟模块化封装
🧩 实现逻辑:
- 使用 IIFE 创建一个模块;
- 内部维护私有变量
modal; - 对外暴露
show()和hide()方法; - 实现统一访问接口。
🧩 实现代码:
const ModalSingleton3 = (function () {
let modal = null; // 私有变量,用于存储唯一的弹窗实例
function createModal() {
modal = document.createElement('div');
modal.id = 'modal';
modal.innerHTML = '我是一个 Modal(方式三)';
document.body.appendChild(modal);
console.log('方式三:创建了弹窗'); // 第一次创建实例时打印日志
}
return {
show() {
if (!modal) createModal(); // 如果没有实例,则创建新的
modal.style.display = 'block'; // 显示弹窗
},
hide() {
if (!modal) createModal(); // 即使是隐藏操作,也要确保实例存在
modal.style.display = 'none'; // 隐藏弹窗
}
};
})();
关键在于:模块模式提供了良好的封装性,所有内部状态都被隐藏在模块内部,对外仅暴露必要的接口(如
show()和hide())。这种方式非常适合工具类或插件开发,因为它能够有效地管理内部状态并提供简洁的外部接口。
优点:
- 封装性强,对外只暴露必要的接口;
- 适合做工具类模块;
- 可扩展为配置化弹窗。
绑定按钮事件(统一测试)
document.getElementById('open1').addEventListener('click', () => {
const modal = new ModalSingleton1();
modal.style.display = 'block';
});
document.getElementById('open2').addEventListener('click', () => {
const modal = new ModalSingleton2().getModal();
modal.style.display = 'block';
});
document.getElementById('open3').addEventListener('click', () => {
ModalSingleton3.show();
});
document.getElementById('close').addEventListener('click', () => {
const modal1 = new ModalSingleton1();
modal1.style.display = 'none';
const modal2 = new ModalSingleton2().getModal();
modal2.style.display = 'none';
ModalSingleton3.hide();
});
效果图:
📌 总结对比表
| 方法 | 实现方式 | 是否使用类 | 适用场景 | 优点 |
|---|---|---|---|---|
| 闭包 + IIFE | 函数闭包 | ❌ | 快速原型、小型项目 | 简洁、轻量 |
| 类 + 静态属性 | class(面向对象写法) | ✅ | 中大型项目、面向对象开发 | 结构清晰、可扩展 |
| 模块模式 | IIFE + 暴露接口 | ❌ | 工具类封装、配置化组件 | 接口统一、封装性强 |
总结
单例模式的本质是“全局唯一 + 延迟加载”,我们可以用闭包、类或模块模式来实现它。不同方式适合不同项目结构和开发习惯,理解它们的逻辑和适用场景,有助于写出更优雅、更健壮的代码。