单例模式学习笔记
这是一份关于前端设计模式中“单例模式” (Singleton Pattern) 的核心概念、应用场景及代码实现的学习笔记。
一、 什么是单例模式?
单例模式是一种确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例的设计模式。
核心特点:
- 唯一实例:一个构造函数无论被调用多少次(
new),最终返回的都是指向同一块内存地址的同一个对象。 - 全局访问:通常会提供一个静态方法(如
getInstance())作为获取该唯一实例的统一入口。 - 实例恒等:通过该模式获取的任意两个“实例”变量,它们是完全相等的 (
===)。
javascript
// 假设 Bus 是一个单例类
const bus1 = Bus.getInstance();
const bus2 = Bus.getInstance();
console.log(bus1 === bus2); // 输出: true二、 为什么要使用单例模式?
保证行为一致性
- 当一个对象需要被系统中的不同部分共享,且这些部分需要操作同一个状态时,单例模式可以确保它们访问的是同一个对象,从而避免数据不一致。
- 经典例子:浏览器的
window对象、localStorage和sessionStorage。你在任何地方访问localStorage,访问的都是同一个存储对象,这保证了数据存取的统一性。如果可以创建多个localStorage实例,就会导致数据混乱。
节约系统资源
- 由于系统中永远只存在一个实例,因此避免了重复创建对象带来的内存开销,在某些场景下可以有效节约内存。
三、 问题场景:未使用单例模式的事件总线 (EventBus)
假设我们有两个模块,它们分别引入并创建了 EventBus 的实例:
- 模块 A:
import EventBus from './bus'; const bus1 = new EventBus(); bus1.on('event', callback); - 模块 B:
import EventBus from './bus'; const bus2 = new EventBus(); bus2.emit('event');
问题:bus2 无法触发 bus1 上监听的事件。
原因:bus1 和 bus2 是通过 new 关键字创建的两个独立对象,它们指向不同的内存空间,内部存储事件的 events 属性也是完全独立的。因此,在一个实例上注册的监听器,在另一个实例上无法被触发。
javascript
const bus1 = new EventBus();
const bus2 = new EventBus();
console.log(bus1 === bus2); // 输出: false四、 如何实现一个单例模式
以下是通过 TypeScript/ES6 Class 来实现 EventBus 单例模式的步骤。
核心三步:
- 私有化构造函数:使用
private constructor()来防止外部通过new关键字直接创建实例。 - 创建私有静态属性:使用
private static instance来保存唯一的实例。 - 创建公共静态方法:提供
public static getInstance()方法作为获取实例的唯一入口。
代码实现:
typescript
class EventBus {
// 2. 创建一个私有的、静态的属性来存储唯一的实例
private static instance: EventBus | null = null;
// 推荐:将内部属性也私有化,防止外部直接操作
private events: { [key: string]: Array<Function> };
// 1. 将构造函数私有化,防止外部使用 `new` 创建实例
private constructor() {
this.events = {};
console.log("EventBus instance created!"); // 这句话只会在第一次调用时打印
}
// 3. 提供一个公共的、静态的方法来获取实例
public static getInstance(): EventBus {
// 如果实例不存在,则创建一个新实例并保存
// 如果实例已存在,则直接返回保存的实例
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
// 更简洁的写法 (逻辑同上)
public static getInstance_short(): EventBus {
// 如果 this.instance 是 null 或 undefined,则执行右侧的 new EventBus() 并赋值
// 否则直接返回 this.instance
return EventBus.instance ||= new EventBus();
}
// 其他公共方法 (on, emit, off...)
public on(name: string, fn: Function) {
// ...
}
public emit(name: string, ...args: any[]) {
// ...
}
}
// --- 使用方式 ---
const bus1 = EventBus.getInstance();
const bus2 = EventBus.getInstance();
// new EventBus(); // 这里会直接报错,因为构造函数是私有的
console.log(bus1 === bus2); // 输出: true五、 总结
通过将 EventBus 改造为单例模式后,无论在项目的哪个角落、调用多少次 EventBus.getInstance(),我们得到的都是最初创建的那一个 EventBus 实例。
这样就完美解决了之前的问题:
- 内存被节约:只创建了一个对象。
- 行为保持一致:现在
bus2可以成功触发bus1上监听的事件,因为它们本质上是同一个对象。
单例模式是保证对象唯一性的强大工具,特别适用于全局状态管理、工具类和需要共享资源等场景。