一、 单例模式是什么?
保证一个类仅有一个实例,并提供一个全局访问点来获取这个实例。
二、 经典单例的 TypeScript 实现
要实现一个单例模式,我们需要做到三点:
- 构造函数必须是私有的 (
private constructor
),防止外部通过new
关键字随意创建实例。 - 类内部需要持有一个静态的、私有的自身实例。
- 提供一个公开的、静态的方法 (
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开发干货