【TS 设计模式完全指南】从“入门”到“劝退”,彻底搞懂单例模式

一、 单例模式是什么?

保证一个类仅有一个实例,并提供一个全局访问点来获取这个实例。

二、 经典单例的 TypeScript 实现

要实现一个单例模式,我们需要做到三点:

  1. 构造函数必须是私有的 (private constructor),防止外部通过 new 关键字随意创建实例。
  2. 类内部需要持有一个静态的、私有的自身实例。
  3. 提供一个公开的、静态的方法 (getInstance),用于获取这个唯一的实例。
typescript 复制代码
class AppConfig {
  // 1. 持有私有的静态实例
  private static instance: AppConfig;

  private config: Record<string, any>;

  // 2. 将构造函数私有化
  private constructor() {
    console.log("读取配置文件... (这只会被打印一次)");
    this.config = {
      version: "1.0.0",
      server: "https://api.example.com",
    };
  }

  public getConfig(key: string): any {
    return this.config[key];
  }

  // 3. 提供公开的静态方法获取实例
  public static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
}

// ---- 如何使用 ----

// const errorConfig = new AppConfig(); // ❌ 错误: "AppConfig" 的构造函数是私有的。TS 在编译期就阻止了你!

const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();

console.log(config1 === config2); // true,它们是同一个实例!

const serverUrl = config1.getConfig("server");
console.log(`服务器地址: ${serverUrl}`);

这就是经典的懒汉式单例 (Lazy Initialization)。只有在第一次调用 getInstance() 时,实例才会被创建。TypeScript 的 private constructor 更是为我们提供了编译时的安全保障!

三、"饿汉式" vs "懒汉式"

除了上面的懒汉式,还有一种实现方式叫饿汉式。它在类加载时就立即创建实例,不管你用不用。

typescript 复制代码
class EagerAppConfig {
    // 在类加载时就直接创建实例
    private static readonly instance: EagerAppConfig = new EagerAppConfig();

    private constructor() {
        console.log('饿汉式:配置文件已加载!');
    }

    public static getInstance(): EagerAppConfig {
        return EagerAppConfig.instance;
    }
}

// 即使还没调用 getInstance,构造函数里的 log 也会被打印出来
//const eagerConfig = EagerAppConfig.getInstance();

两者对比:

  • 懒汉式
    • 优点:延迟加载,节省资源。如果一直用不到这个实例,就不会创建它。
    • 缺点:在多线程环境下需要处理同步问题(不过在 JS/TS 的单线程事件循环模型中,实例化本身的竞态条件不是主要问题)。第一次获取实例时会稍慢。
  • 饿汉式
    • 优点:实现简单,天生线程安全。获取实例速度快。
    • 缺点:类加载时就初始化,可能造成资源浪费,尤其当实例创建很耗时,但应用又不一定会使用它时。

在 TS/JS 环境中,由于其模块加载机制,我们还有一种更简洁的"单例"实现方式。

四、JS/TS特有的单例模式实现

ES6 模块有一个重要特性:模块内的代码只会在第一次被导入时执行一次。之后再 import 同一个模块,只会得到缓存的导出结果。我们可以利用这个特性,轻松实现一个单例。

typescript 复制代码
// logger.ts
class Logger {
    private logs: string[] = [];

    constructor() {
        console.log('Logger 初始化了!(只会发生一次)');
    }

    public log(message: string) {
        const timestamp = new Date().toISOString();
        this.logs.push(`[${timestamp}] ${message}`);
        console.log(`[Logger]: ${message}`);
    }

    public printLogs() {
        console.log(this.logs);
    }
}

// 直接实例化并导出
export const logger = new Logger();
// 这样在任何地方 import { logger } 都会得到同一个实例

// ---- 在其他文件中使用 ----
// a.ts
import { logger } from './logger';
logger.log("模块 A 的消息");

// b.ts
import { logger } from './logger';
logger.log("模块 B 的消息");
logger.printLogs(); // 会打印出模块 A 和 B 的两条消息

这种方式代码量最少,也最直观。它实际上是一种饿汉式的实现,非常适合那些创建开销不大且必定会被用到的场景。对于绝大多数 TS/JS 应用的简单全局实例需求,这通常是最佳选择

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

相关推荐
JS.Huang1 小时前
【JavaScript】原生函数
开发语言·javascript·ecmascript
533_2 小时前
[vue] dayjs 显示实时时间
前端·javascript·vue.js
ftpeak2 小时前
JavaScript性能优化实战
开发语言·javascript·性能优化
Tiny_React3 小时前
智能体设计模式-附录 C - 智能体框架快速概览
设计模式
一个很帅的帅哥3 小时前
JavaScript事件循环
开发语言·前端·javascript
云枫晖3 小时前
Webapck系列-初识Webpack
前端·javascript
jiangzhihao05153 小时前
升级到webpack5
前端·javascript·vue.js
哆啦A梦15884 小时前
36 注册
前端·javascript·html
WujieLi4 小时前
初识 Vite+:一文了解 Rust 驱动的新一代前端工具链
javascript·rust·vite
可触的未来,发芽的智生4 小时前
新奇特:神经网络速比器,小镇债务清零的算法奇缘
javascript·人工智能·python