JavaScript设计模式应在代码量超3000行、多人协作或频繁连锁崩溃时使用;单例确保全局唯一实例,观察者解耦事件通知,二者组合构成状态管理库核心。

JavaScript 设计模式不是为了炫技,而是当代码量超过 3000 行、多人协作修改同一模块、或者频繁出现“改一处崩三处”时,你才真正需要它。单例和观察者模式尤其高频——前者管全局状态的唯一性,后者解耦事件通知逻辑。
单例模式:如何确保一个类只生成一个实例
常见错误是直接用 const instance = new MyClass(),看似只创建一次,但模块被多次 import 时(尤其在 Webpack 的 HMR 或测试环境里),instance 可能被重复初始化。真正的单例必须控制构造入口。
- 用闭包 + 静态属性是最稳妥的方式:
class Logger { constructor() { if (Logger.instance) return Logger.instance; this.logs = []; Logger.instance = this; } } // 使用时始终调用 new,内部拦截 const logger1 = new Logger(); const logger2 = new Logger(); console.log(logger1 === logger2); // true - ES6 模块天然单例,更推荐导出实例而非类:
// logger.js export const logger = new (class { constructor() { this.logs = []; } })();这样无论多少个文件import { logger },拿到的都是同一个对象 - 注意:如果类依赖外部参数(如配置项),不能简单用
if (Logger.instance),得加判断逻辑或改用工厂函数封装初始化条件
观察者模式:如何避免组件间硬编码调用
典型场景是表单校验后通知提交按钮更新状态,或 WebSocket 收到消息后刷新多个视图。直接写 button.update(); chart.redraw(); 会导致高度耦合,改一个就要动一堆地方。
- 核心是分离“谁发消息”和“谁收消息”:
class EventEmitter { constructor() { this.events = {}; } on(type, fn) { this.events[type] = this.events[type] || []; this.events[type].push(fn); } emit(type, data) { (this.events[type] || []).forEach(fn => fn(data)); } } - 实际使用中,不要让业务逻辑直接依赖
EventEmitter类,而是封装成语义化事件:// auth.js export const authBus = new EventEmitter(); // login.js authBus.emit('login-success', { token, user }); // header.js authBus.on('login-success', ({ user }) => updateAvatar(user)); - 容易漏掉的是取消监听 —— 组件卸载时必须调用
off()或用 WeakMap 管理 listener 生命周期,否则引发内存泄漏。现代框架(React/Vue)的 effect cleanup 就是干这个的
单例 + 观察者组合:为什么全局状态管理库都这么写
像 Redux store 或 Pinia 实际就是单例 + 观察者混合体:store 是唯一实例,subscribe 就是观察者注册。自己手写时最容易忽略的是事件触发时机和同步/异步边界。
立即学习“Java免费学习笔记(深入)”;
- 同步 emit 容易导致“通知过程中又触发新通知”,形成调用栈爆炸;建议加队列或用
Promise.resolve().then(() => emit())推迟到微任务 - 单例对象若暴露了可写属性(如
store.state = {}),外部直接改会导致观察者无法捕获变化 —— 必须配合Object.freeze或 Proxy 拦截 set - Node.js 环境下要注意 CommonJS 和 ESM 模块缓存机制差异:ESM 的
export default是活绑定,CommonJS 的module.exports是值拷贝,混用时单例可能失效
真正难的不是写出符合 UML 图的模式代码,而是判断什么时候不该用——比如两个函数只在同一个文件里调用,硬套观察者反而增加理解成本。模式是工具,不是教条。










