1. 核心定义
单例模式(Singleton Pattern)是一种确保一个类只有一个实例,并提供一个全局访问点来获取该实例的设计模式。
- 生活比喻 🏫:就像一所学校只有一个校长,单例模式确保程序中某个特定的类只有一个“分身”。
2. TypeScript 实现三要素
要创建一个单例类,通常需要三个关键部分:
private static instance:一个私有的静态变量,用于存储唯一的实例。
private constructor():一个私有的构造函数,防止外部通过new关键字随意创建新实例。
public static getInstance():一个公有的静态方法,作为获取实例的唯一入口。它会检查实例是否存在,若不存在则创建,否则直接返回已有的实例。
3. 优缺点与应用场景
- 优点 👍
-
- 保证实例唯一:有效避免了多个实例造成的逻辑混乱或资源冲突。
- 节约系统资源:对于创建开销大的对象(如数据库连接),可以复用实例,降低消耗。
- 提供全局访问点:方便在程序的任何地方访问和管理共享状态。
- 缺点 👎
-
- 对单元测试不友好:全局状态使得测试隔离变得困难。
- 违反单一职责原则:类既要负责业务逻辑,又要负责管理自己的实例。
- 隐藏依赖关系:在类内部直接调用单例,使得依赖关系不明确。
- 适用场景 ✅
-
- 需要全局唯一的组件,如日志记录器 (Logger) 、全局配置管理器 (ConfigManager) 或数据库连接池。
- 不适用场景 ❌
-
- 与特定用户或会话绑定的对象,如购物车。每个用户都需要一个独立的购物车实例。
4. 两大基本类型
- 饿汉式 (Eager Initialization)
-
- 核心:在类加载时就立即创建实例。
- 优点:实现简单,天生线程安全。
- 缺点:若实例从未使用,会造成资源浪费。
- 懒汉式 (Lazy Initialization)
-
- 核心:在第一次调用
getInstance()时才创建实例(延迟加载)。 - 优点:按需创建,节约资源。
- 缺点:在传统多线程语言中需处理复杂的线程安全问题。
- 核心:在第一次调用
5. "加锁" 与线程安全问题 (TypeScript 环境)
- 同步单例
-
- 结论:不需要加锁。
- 原因:JavaScript 的单线程事件循环机制保证了代码在执行
if...new检查和创建的过程中不会被中断,天然避免了竞争问题。
- 异步单例
-
- 结论:需要一种“锁”机制。
- 场景:当单例的初始化包含
async/await操作时(如读取文件、网络请求)。 - 实现:最佳实践是使用一个共享的 Promise 变量充当“锁”,确保初始化逻辑只被完整执行一次。
一句话总结 💡:在 TypeScript 中,同步单例很简单;异步单例则必须用 Promise 来为初始化过程“加锁”。
// 饿汉模式
class EagerSingleton {
// 1. 关键:在类加载时就直接创建实例,并设为 readonly 防止被修改
private static readonly instance: EagerSingleton = new EagerSingleton();
// 2. 将构造函数设为私有,防止外部通过 `new` 创建
private constructor() {
console.log("饿汉式单例:实例在类加载时已创建。🚀");
}
// 3. 提供一个公有的静态方法,直接返回已经创建好的实例
public static getInstance(): EagerSingleton {
return EagerSingleton.instance;
}
// 示例方法
public showMessage(): void {
console.log("这是一个来自饿汉式单例的消息。");
}
}
// --- 使用方法 ---
console.log("--- 开始使用饿汉式单例 ---");
const eager1 = EagerSingleton.getInstance();
const eager2 = EagerSingleton.getInstance();
eager1.showMessage();
// 验证是否是同一个实例
console.log("eager1 === eager2:", eager1 === eager2); // true
console.log("--- 饿汉式单例使用结束 ---");
JavaScript
//懒汉模式
class LazySingleton {
// 1. 关键:声明一个静态实例,但不立即初始化,默认为 null
private static instance: LazySingleton | null = null;
// 2. 将构造函数设为私有
private constructor() {
console.log("懒汉式单例:实例在首次调用 getInstance() 时创建。🤔");
}
// 3. 提供公有静态方法,并在其中检查实例是否存在
public static getInstance(): LazySingleton {
// 如果实例不存在,才进行创建
if (!LazySingleton.instance) {
LazySingleton.instance = new LazySingleton();
}
// 返回实例
return LazySingleton.instance;
}
// 示例方法
public showMessage(): void {
console.log("这是一个来自懒汉式单例的消息。");
}
}
// --- 使用方法 ---
console.log("\n--- 开始使用懒汉式单例 ---");
console.log("准备第一次获取实例...");
const lazy1 = LazySingleton.getInstance(); // 此时才会打印构造函数中的消息
console.log("准备第二次获取实例...");
const lazy2 = LazySingleton.getInstance(); // 不会再打印构造函数中的消息
lazy1.showMessage();
// 验证是否是同一个实例
console.log("lazy1 === lazy2:", lazy1 === lazy2); // true
console.log("--- 懒汉式单例使用结束 ---");
JavaScript
// 带锁懒汉模式
class AsyncSafeSingleton {
private static instance: AsyncSafeSingleton | null = null;
// 这就是我们的“锁”:一个代表初始化过程的 Promise
private static initializingPromise: Promise<AsyncSafeSingleton> | null = null;
private constructor() {
console.log("异步安全单例:开始初始化... (只会执行一次)");
}
// 模拟一个异步的初始化过程
private async initialize(): Promise<void> {
// 假设这里有 await db.connect(), await fs.readFile() 等
await new Promise(resolve => setTimeout(resolve, 1000));
}
public static getInstance(): Promise<AsyncSafeSingleton> {
// 如果实例已存在,直接返回
if (this.instance) {
return Promise.resolve(this.instance);
}
// 如果正在初始化(“锁”被占用),则等待它完成
if (this.initializingPromise) {
return this.initializingPromise;
}
// 第一个调用者,创建“锁”并开始初始化
this.initializingPromise = (async () => {
const instance = new AsyncSafeSingleton();
await instance.initialize(); // 执行异步初始化
this.instance = instance; // 将实例赋值
this.initializingPromise = null; // 释放“锁”
console.log("异步安全单例:初始化完成!");
return instance;
})();
return this.initializingPromise;
}
}
// --- 使用方法 ---
console.log("并发调用 getInstance:");
AsyncSafeSingleton.getInstance().then(s => console.log("第一次调用完成"));
AsyncSafeSingleton.getInstance().then(s => console.log("第二次调用完成"));
AsyncSafeSingleton.getInstance().then(s => console.log("第三次调用完成"));