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

相关推荐
gnip16 分钟前
脚本加载失败重试机制
前端·javascript
yujkss44 分钟前
23种设计模式之【享元模式】-核心原理与 Java实践
java·设计模式·享元模式
不搞学术柒柒1 小时前
设计模式-常见设计原则篇
设计模式
颜酱1 小时前
实现一个mini编译器,来感受编译器的各个流程
前端·javascript·编译器
Ares-Wang2 小时前
Vue3》》 ref 获取子组件实例 原理
javascript·vue.js·typescript
爱上妖精的尾巴2 小时前
5-16WPS JS宏 map数组转换迭代应用-1(一维嵌套数组结构重组)
开发语言·前端·javascript·wps·jsa
咖啡の猫2 小时前
Vue 简介
前端·javascript·vue.js
m0_526119403 小时前
pdf文件根据页数解析成图片 js vue3
前端·javascript·pdf
sorryhc4 小时前
手写一个Webpack HMR插件——彻底搞懂热更新原理
前端·javascript·前端工程化
xiaoxiao无脸男4 小时前
three.js
开发语言·前端·javascript