javascript设计模式有哪些_如何实现单例与观察者模式?

单例应确保全局唯一且延迟初始化,推荐模块作用域+闭包封装;观察者需防内存泄漏、用常量管理事件名;单例+观察者组合时须注意初始化时机、生命周期绑定与异步通知。

JavaScript 单例模式:用闭包还是 class?

单例的核心是「全局唯一实例 + 延迟初始化」,不是「只写一个对象字面量」。直接 const instance = { ... } 看似简单,但无法控制构造逻辑、无法延迟加载、无法防止多次 new,也不支持依赖注入。

推荐用模块作用域 + 闭包封装,兼顾私有状态与可测试性:

const Singleton = (function() {
  let instance = null;
  function createInstance() {
    return {
      data: [],
      add(item) { this.data.push(item); },
      getCount() { return this.data.length; }
    };
  }
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

注意点:

  • 不要在 class 中靠 static instance + 构造器判空——ES6 class 无私有构造器,new Singleton() 仍可绕过控制
  • 如果必须用 class,把构造器设为私有(用 Symbol 或 WeakMap 标记),并在静态方法中统一入口
  • Webpack / Vite 的模块缓存天然保证「同一 import 路径只执行一次」,所以导出对象字面量在大多数场景下也符合单例语义,但不可用于需要传参初始化的场景

观察者模式:EventEmitter 是万能解吗?

原生 EventTarget(浏览器)和 events.EventEmitter(Node.js)是观察者模式的成熟实现,但它们不解决「谁负责订阅/取消订阅」「事件名拼错静默失败」「监听器内存泄漏」这些实际问题。

手写轻量版更可控,尤其适合组件通信或状态管理内部使用:

class Observer {
  constructor() {
    this._callbacks = new Map();
  }
  subscribe(event, fn) {
    if (!this._callbacks.has(event)) {
      this._callbacks.set(event, new Set());
    }
    this._callbacks.get(event).add(fn);
  }
  unsubscribe(event, fn) {
    const fns = this._callbacks.get(event);
    if (fns) fns.delete(fn);
  }
  notify(event, ...args) {
    const fns = this._callbacks.get(event);
    if (fns) fns.forEach(fn => fn(...args));
  }
}

关键细节:

  • Set 而非数组存监听器,避免重复 subscribe 导致多次触发
  • 务必提供 unsubscribe,否则 Vue/React 组件卸载时监听器残留会引发内存泄漏
  • 不要用字符串拼接事件名(如 user:update:profile),改用常量对象管理:const EVENTS = { USER_UPDATE: 'user:update' },避免拼写错误难排查

单例 + 观察者组合:状态中心常见误用

很多项目用「单例 Store + 内置 EventEmitter」做状态管理,但容易忽略两个边界:

  • 单例 Store 的初始化时机不对——比如在模块顶层执行 new Store(),但依赖的 API 客户端尚未配置完成,导致初始化失败且无重试机制
  • 观察者未绑定到生命周期——例如在 React 组件中 useEffect(() => { store.subscribe('data', handler) }, []),但忘记在 cleanup 中 unsubscribe,handler 会持续持有旧组件闭包,造成内存泄漏和重复渲染
  • 事件通知同步执行——notify() 是同步的,如果某个监听器执行慢或报错,会阻塞后续监听器;需考虑加 setTimeout(() => fn(), 0) 或用 queueMicrotask 转为异步(但会丢失调用栈上下文)

该选库还是手写?看这三点

是否引入 rxjsmitteventemitter3,取决于三个硬指标:

  • 是否需要取消订阅的 token 机制(mitt 不支持,rxjsSubscription 支持)
  • 是否要求事件通配符(如 on('user:*')),eventemitter3 支持,原生 EventTarget 不支持
  • 构建产物体积敏感度——mitt 只有 200B,而 rxjs 默认打包进几 KB,若仅需 emit/listen,手写 20 行足够

真正难的不是模式本身,而是谁持有订阅关系、什么时候清理、错误是否透出、事件是否可追溯——这些不会因为用了设计模式就自动解决。