Skip to content

单例模式学习笔记

这是一份关于前端设计模式中“单例模式” (Singleton Pattern) 的核心概念、应用场景及代码实现的学习笔记。

一、 什么是单例模式?

单例模式是一种确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例的设计模式。

核心特点:

  • 唯一实例:一个构造函数无论被调用多少次(new),最终返回的都是指向同一块内存地址的同一个对象。
  • 全局访问:通常会提供一个静态方法(如 getInstance())作为获取该唯一实例的统一入口。
  • 实例恒等:通过该模式获取的任意两个“实例”变量,它们是完全相等的 (===)。
javascript
// 假设 Bus 是一个单例类
const bus1 = Bus.getInstance();
const bus2 = Bus.getInstance();

console.log(bus1 === bus2); // 输出: true

二、 为什么要使用单例模式?

  1. 保证行为一致性

    • 当一个对象需要被系统中的不同部分共享,且这些部分需要操作同一个状态时,单例模式可以确保它们访问的是同一个对象,从而避免数据不一致。
    • 经典例子:浏览器的 window 对象、localStoragesessionStorage。你在任何地方访问 localStorage,访问的都是同一个存储对象,这保证了数据存取的统一性。如果可以创建多个 localStorage 实例,就会导致数据混乱。
  2. 节约系统资源

    • 由于系统中永远只存在一个实例,因此避免了重复创建对象带来的内存开销,在某些场景下可以有效节约内存。

三、 问题场景:未使用单例模式的事件总线 (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 上监听的事件。

原因bus1bus2 是通过 new 关键字创建的两个独立对象,它们指向不同的内存空间,内部存储事件的 events 属性也是完全独立的。因此,在一个实例上注册的监听器,在另一个实例上无法被触发。

javascript
const bus1 = new EventBus();
const bus2 = new EventBus();

console.log(bus1 === bus2); // 输出: false

四、 如何实现一个单例模式

以下是通过 TypeScript/ES6 Class 来实现 EventBus 单例模式的步骤。

核心三步:

  1. 私有化构造函数:使用 private constructor() 来防止外部通过 new 关键字直接创建实例。
  2. 创建私有静态属性:使用 private static instance 来保存唯一的实例。
  3. 创建公共静态方法:提供 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 上监听的事件,因为它们本质上是同一个对象。

单例模式是保证对象唯一性的强大工具,特别适用于全局状态管理、工具类和需要共享资源等场景。