- Published on
设计模式(五)之单例模式
- Authors

- Name
- 葛颂
单例模式(Singleton)限制某个类只能有一个实例,并提供一个全局访问点。适合进程内真正唯一的资源:配置读取器、连接池入口、应用级日志器。
它不适合「这个页面上刚好只有一个」。那是当前数据,不是类型约束。
类单例
把构造函数收起来,只通过 getInstance 取对象:
class AppConfig {
private static instance: AppConfig | undefined
readonly apiBase: string
private constructor(apiBase: string) {
this.apiBase = apiBase
}
static getInstance() {
if (!AppConfig.instance) {
const apiBase = process.env.API_BASE ?? 'https://example.com'
AppConfig.instance = new AppConfig(apiBase)
}
return AppConfig.instance
}
}
const a = AppConfig.getInstance()
const b = AppConfig.getInstance()
a === b // true
private constructor 阻止外部 new AppConfig()。第一次调用创建实例,之后都返回同一引用。
模块单例
在 TypeScript / ES Module 里,模块本身只求值一次。多数场景直接导出实例更简单:
class Logger {
info(message: string) {
console.log(message)
}
}
export const logger = new Logger()
任何文件 import { logger } 拿到的都是同一个对象,不必再写 getInstance。类单例主要用在必须从类上取实例、或实例化时机要延迟到第一次调用时。
现实场景
- 应用日志:全站共用一个 logger,统一级别、输出位置和请求 ID,而不是每个模块
new Logger()。 - 数据库连接池:进程里只建一个池,请求从池里借连接。每个请求再新建池会耗尽数据库连接数。
- 浏览器平台对象:
window、document、localStorage在一个页面里本来就只有一份,业务代码直接使用这份唯一实例。 - 功能开关配置:启动时读一次远程配置并缓存在单例里,后续请求复用,避免每次打配置中心。
什么时候用
- 重复创建会出错或很贵:数据库连接池、硬件句柄、进程级缓存。
- 调用链很深,逐层传递这一个依赖会让函数签名全是同一个参数。
如果只是怕「忘记传递依赖」,优先考虑在组合根创建一次,再显式注入。那样测试可以换实现,生产路径也仍然只有一个实例。
注意
- 单例是隐式全局状态。测试之间会互相污染,需要提供重置口,或改成注入:
class AppConfig {
private static instance: AppConfig | undefined
static resetForTest() {
AppConfig.instance = undefined
}
}
这个方法只应出现在测试里。
- 懒加载单例若在并发任务里第一次被同时调用,可能创建出两份。浏览器主线程通常不必担心;Worker 或多实例 Node 集群里,每个线程 / 进程仍各有一份,它不是跨进程唯一。
- 不要把用户会话、当前购物车做成单例。它们按请求或按用户存在,全局一份会串数据。