单例模式

2026 年 2 月 18 日 星期三
2

单例模式

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 工程中,模块级导出往往是更实用、更易测试的单例形式

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...