单例模式
Q1:什么是单例模式?
单例模式是一种创建型设计模式,它确保一个类在整个应用生命周期中只有一个实例,并提供一个全局访问点来获取该实例。
Q2:单例模式的核心特征是什么?
- 全局唯一性:应用中只存在一个实例
- 延迟实例化(常见但非必须):通常第一次使用时才创建
- 全局访问:提供静态方法(或模块导出)获取实例
Q3:单例模式通常解决什么问题?
- 避免重复创建“昂贵对象”(例如:数据库连接池、缓存、配置中心客户端、日志器)
- 保证共享资源的唯一协调者(例如:全局事件总线、任务调度器、锁/限流器)
- 统一管理全局配置或运行时状态(但要谨慎,容易变成“全局变量”)
Q4:单例模式有哪些常见缺点/风险?
- 隐藏依赖:代码看起来没传参,实际依赖全局单例,耦合度上升
- 不利于测试:状态容易在测试用例间污染,需要手动重置/隔离
- 难以扩展:多实例需求出现时(多租户/多环境/多连接),改造成本高
- 滥用导致全局状态:把单例当“万能仓库”会让系统变得不可控
Q5:单例与“全局变量”有什么区别?
- 全局变量:任意读写,缺少生命周期与访问控制
- 单例:把实例创建与访问封装在类型/模块内部,可约束构造方式、暴露受控 API,并可在需要时增加懒加载、初始化逻辑等
Q6:单例模式的几种实现方式(TypeScript 示例)?
1)饿汉式(Eager Initialization)
类加载时就创建实例;实现简单、天然“线程安全”,但可能在不使用时也占用资源。
class EagerSingleton {
private static instance = new EagerSingleton();
private constructor() {}
static getInstance(): EagerSingleton {
return EagerSingleton.instance;
}
}2)懒汉式(Lazy Initialization)
第一次使用时才创建实例;节省资源,但在多线程语言里需要考虑线程安全(JS 一般单线程,但多 Worker/多进程场景要换思路)。
class LazySingleton {
private static instance: LazySingleton;
private constructor() {}
static getInstance(): LazySingleton {
if (!LazySingleton.instance) {
LazySingleton.instance = new LazySingleton();
}
return LazySingleton.instance;
}
}3)双重检查锁定(Double-Checked Locking)
在多线程语言中常用于降低锁开销;在 JS/TS 里通常不需要“锁”,但如果你在运行时环境中确实存在并发初始化(例如某些跨线程共享方案),需要使用更可靠的并发原语(而不是布尔标记)。
下面示例仅用于展示结构思路:
class DoubleCheckSingleton {
private static instance: DoubleCheckSingleton;
private static creating = false;
private constructor() {}
static getInstance(): DoubleCheckSingleton {
if (!DoubleCheckSingleton.instance) {
if (!DoubleCheckSingleton.creating) {
DoubleCheckSingleton.creating = true;
DoubleCheckSingleton.instance = new DoubleCheckSingleton();
DoubleCheckSingleton.creating = false;
}
}
return DoubleCheckSingleton.instance;
}
}Q7:在前端/Node.js 中,最“天然”的单例实现是什么?
ES Module / CommonJS 模块本身就天然单例:同一个模块通常只会初始化一次,然后被缓存复用。
// logger.ts
export const logger = {
info(msg: string) {
console.log("[info]", msg);
},
};在工程里直接 import { logger } from "./logger",多数情况下就已经满足“单例”的需求,而且比 class + getInstance() 更直观、更易测(也更容易替换 mock)。
Q8:什么时候更推荐用依赖注入(DI)而不是单例?
- 需要在不同环境/租户下创建不同实例(多配置、多连接)
- 希望更容易 mock / 替换实现(单元测试、A/B)
- 项目已有 IoC/DI 框架(例如 NestJS),容器管理生命周期比手写单例更清晰
单例不是“不能用”,而是要在生命周期清晰、实例确实应全局唯一时使用。
Q9:单例需要“线程安全”吗?
- 浏览器 JS 通常单线程:大多数“线程安全”问题不存在
- Node.js 单进程事件循环也是单线程执行 JS,但可能存在:
- 多进程(cluster / pm2)→ 每个进程各自一份“单例”
- Worker Threads → 每个 Worker 各自一份(除非你做了共享内存/通信协调)
结论:单例保证的范围通常是“当前运行上下文/进程内唯一”,跨进程唯一需要借助外部系统(Redis 锁、数据库、分布式协调等)。
Q10:单例模式的实践建议是什么?
- 只把“确实全局唯一”的对象做成单例:日志器、配置读取、缓存适配器等
- 避免单例里塞业务状态,减少测试污染
- 优先模块导出单例(更易替换、更简洁),只有在需要私有构造/继承控制时再用
class getInstance() - 如果单例需要异步初始化,考虑显式的
init()或工厂函数,避免“半初始化”状态
小结
- 单例模式的目标是:全局唯一实例 + 统一访问点
- 常见实现:饿汉式、懒汉式(以及多线程语境下的双重检查)
- 在 TS/JS 工程中,模块级导出往往是更实用、更易测试的单例形式