单例模式
一、什么是 单例模式(Singleton Pattern)?
✅ 定义:
单例模式 是一种设计模式,确保一个类在整个程序运行期间只存在一个实例,并提供一个全局访问点。
🧩 核心特点:
- 私有构造函数(防止外部 new)
- 静态方法或属性 返回唯一实例
- 实例只创建一次
📌 JavaScript 中的经典单例示例:
class Database {
constructor() {
if (Database.instance) {
return Database.instance; // 已存在就返回
}
this.data = {};
Database.instance = this; // 保存唯一实例
}
getData(key) { return this.data[key]; }
setData(key, value) { this.data[key] = value; }
}
// 使用
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true → 是同一个对象!
✅ 这就是“单例”:无论你 new 多少次,拿到的都是同一个对象。
二、ESM 模块是单例
✅ 简单说:
在 ES 模块(ESM)系统中,每个模块文件(
.js文件)只会被加载和执行一次。
后续的import都会复用第一次加载的结果,而不是重新执行模块代码。
这就使得 模块内部的状态(变量、函数等)天然具有“单例”特性。
🔍 举个例子
counter.js
// 模块级变量(只初始化一次!)
let count = 0;
export function increment() {
count++;
console.log('Count:', count);
}
export function getCount() {
return count;
}
app1.js
import { increment, getCount } from './counter.js';
increment(); // Count: 1
console.log(getCount()); // 1
app2.js
import { increment, getCount } from './counter.js';
increment(); // Count: 2 ← 注意!不是 1!
console.log(getCount()); // 2
main.js
import './app1.js'; // 执行后 count = 1
import './app2.js'; // 执行后 count = 2
✅ 输出:
Count: 1
1
Count: 2
2
💡 关键:
count变量在counter.js中只声明了一次。
即使多个文件import它,它们共享同一个count变量 —— 这就是 模块单例!
三、为什么 ESM 是单例?技术原理
当你 import 一个模块时,JavaScript 引擎(或打包工具如 Webpack、Vite)会:
-
检查是否已加载该模块
- 如果没有:执行模块代码,缓存其导出结果
- 如果有:直接返回缓存的结果(不再执行模块体)
-
模块作用域是封闭的,但内部状态在多次 import 间保持
这本质上是一种 “懒加载 + 缓存” 机制,保证了模块的执行唯一性。
四、对比:CommonJS(Node.js)也是单例吗?
✅ 是的! CommonJS 同样是单例:
// counter.js (CommonJS)
let count = 0;
exports.increment = () => {
count++;
console.log(count);
};
// a.js
const c = require('./counter');
c.increment(); // 1
// b.js
const c = require('./counter');
c.increment(); // 2 ← 共享状态!
Node.js 会把模块缓存在 require.cache 中,下次直接返回缓存。
五、重要注意事项 ⚠️
1. 单例 ≠ 全局变量安全
- 在 浏览器多 Tab 中:每个 Tab 是独立 JS 环境,不共享模块状态
- 在 Node.js 多进程 中:每个进程有自己的内存,模块不共享
- 但在同一个 JS 执行环境内(如同一个页面、同一个 Node 进程),模块是单例
2. SSR(服务端渲染)中的陷阱
在 Next.js/Nuxt 等 SSR 框架中:
- 如果模块状态在服务端被修改
- 可能导致不同用户请求之间污染数据(因为 Node 进程复用模块)
✅ 解决方案:避免在模块顶层放用户相关状态,或每次请求重置。
✅ 总结
| 概念 | 说明 |
|---|---|
| 单例模式 | 确保一个类只有一个实例,并提供全局访问点 |
| ESM 模块是单例 | 每个 .js 文件只执行一次,多次 import 共享同一份内部状态 |
| 本质 | 模块系统通过缓存机制实现单例行为 |
| 用途 | 适合实现全局配置、计数器、工具库等无状态/共享状态逻辑 |
| 注意 | 不适用于存储用户私有数据(尤其在 SSR 场景) |
💡 一句话记住:
“ESM 模块 = 自动单例” —— 你不需要手动实现单例,模块本身就是这样工作的!
Demo
<button id="loginBtn">按钮</button>
<script>
var num = 1;
var createLoginLayer = function () {
num++;
var div = document.createElement('div');
div.innerHTML = '登录窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
var getSingle = function (fn) {
var result;
return function () {
// getSingle 执行完 reault 没有被回收,而是指向 fn 的返回值,形成闭包
return result || (result = fn.apply(this, arguments));
};
};
var createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = function () {
console.log(num);
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
</script>