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

相关推荐
dsyyyyy11015 小时前
JavaScript变量
开发语言·javascript·ecmascript
kyriewen6 小时前
手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势
前端·javascript·面试
胡志辉的博客7 小时前
深入浅出理解浏览器事件循环:从一道输出题讲到 Chrome 源码
前端·javascript·chrome·chromium·event loop
代码不加糖7 小时前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
退休倒计时7 小时前
【每日一题】LeetCode 53. 最大子数组和 TypeScript
数据结构·算法·leetcode·typescript
懂懂tty7 小时前
Vue2与Vue3之间API差异
前端·javascript·vue.js
小二·8 小时前
Next.js 15 全栈开发实战
开发语言·javascript·ecmascript
Rain5099 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
拾年27510 小时前
从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
前端·javascript·ajax
拉勾科研工作室11 小时前
区块链工程毕业论文题目【249个】
开发语言·javascript