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

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)是观察者模式的成熟实现,但它们不解决「谁负责订阅/取消订阅」「事件名拼错静默失败」「监听器内存泄漏」这些实际问题。
立即学习“Java免费学习笔记(深入)”;
手写轻量版更可控,尤其适合组件通信或状态管理内部使用:
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转为异步(但会丢失调用栈上下文)
该选库还是手写?看这三点
是否引入 rxjs、mitt 或 eventemitter3,取决于三个硬指标:
- 是否需要取消订阅的 token 机制(
mitt不支持,rxjs的Subscription支持) - 是否要求事件通配符(如
on('user:*')),eventemitter3支持,原生EventTarget不支持 - 构建产物体积敏感度——
mitt只有 200B,而rxjs默认打包进几 KB,若仅需 emit/listen,手写 20 行足够
真正难的不是模式本身,而是谁持有订阅关系、什么时候清理、错误是否透出、事件是否可追溯——这些不会因为用了设计模式就自动解决。











