跳到主要内容

单例模式

一、什么是 单例模式(Singleton Pattern)

✅ 定义:

单例模式 是一种设计模式,确保一个类在整个程序运行期间只存在一个实例,并提供一个全局访问点。

🧩 核心特点:

  1. 私有构造函数(防止外部 new)
  2. 静态方法或属性 返回唯一实例
  3. 实例只创建一次

📌 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)会:

  1. 检查是否已加载该模块

    • 如果没有:执行模块代码,缓存其导出结果
    • 如果有:直接返回缓存的结果(不再执行模块体
  2. 模块作用域是封闭的,但内部状态在多次 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>