【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开发干货

相关推荐
正义的大古4 小时前
OpenLayers常用控件 -- 章节六:全屏控件教程
前端·javascript·html·openlayers
lypzcgf4 小时前
Coze源码分析-资源库-删除插件-前端源码-核心组件实现
前端·typescript·前端框架·react·coze·coze插件·智能体平台
Meteors.5 小时前
23种设计模式——原型模式 (Prototype Pattern)详解
设计模式·原型模式
正义的大古6 小时前
OpenLayers常用控件 -- 章节七:测量工具控件教程
前端·javascript·vue.js·openlayers
雲墨款哥7 小时前
一个前端开发者的救赎之路-JS基础回顾(五)-数组
前端·javascript·面试
朱程7 小时前
深入JS(一):手写 Promise
前端·javascript
EndingCoder8 小时前
Electron 性能优化:内存管理和渲染效率
javascript·性能优化·electron·前端框架
牛十二8 小时前
mac-intel操作系统go-stock项目(股票分析工具)安装与配置指南
开发语言·前端·javascript