写 JavaScript 项目时,当状态管理、全局通知这类需求逐渐复杂起来,设计模式会从教科书概念变成真正能减少重复代码的工具。单例模式和观察者模式是其中最常用的两个,前者控制实例数量,后者管理消息流转。理解它们并不是为了背诵定义,而是为了在写全局配置、事件总线、状态容器时能自然选择合适的结构。下面从实现方式、应用场景和组合思路分别展开。

一、单例模式:让一个类只有一个实例
单例模式的核心约束非常直观:无论调用多少次创建逻辑,得到的都应该是同一个对象。这个特性很适合放全局配置、缓存映射、弹窗控制器等需要单一数据源的场景。JavaScript 里最朴素的单例就是一个对象字面量,模块加载时创建一次,后续所有导入都共享同一份引用。
const appConfig = {
apiBase: 'https://api.ipipp.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
export default appConfig;
对象字面量虽然简单,但当实例需要私有状态或惰性初始化时,可以使用类配合静态方法来实现。ES6 之后,class 语法让单例更接近传统写法,同时可以通过闭包隐藏内部数据。下面这个例子中,构造函数不会直接暴露,外部只能通过 getInstance 获取同一个实例。
class DatabasePool {
static instance = null;
constructor(config) {
this.config = config;
this.connections = [];
}
static getInstance(config) {
if (!DatabasePool.instance) {
DatabasePool.instance = new DatabasePool(config);
}
return DatabasePool.instance;
}
connect() {
if (this.connections.length >= this.config.max) {
throw new Error('连接数已达上限');
}
this.connections.push({ id: this.connections.length + 1 });
return this.connections[this.connections.length - 1];
}
}
const pool1 = DatabasePool.getInstance({ max: 5 });
const pool2 = DatabasePool.getInstance({ max: 10 });
console.log(pool1 === pool2); // true
单例模式的好处在于状态集中、内存占用可控,尤其适合连接池、音频管理器这类不希望重复初始化的资源。但它也不是银弹。全局可访问意味着模块之间的隐式耦合增强,单测时有时候需要打补丁或重置状态。一个更稳妥的做法是尽量把单例限制在模块内部,对外只导出方法,减少外部直接修改实例的机会。
二、观察者模式:建立一对多的通知网络
观察者模式解决的是状态变化后如何通知多个对象的问题。目标对象维护一份订阅者列表,当自身状态发生改变时,依次调用每个订阅者的更新逻辑。浏览器里的 addEventListener、Node.js 中的 EventEmitter 都是这种思想的具体实现。在 JavaScript 里,自己实现一个事件中心并不复杂,反而能帮助理解框架背后的事件机制。
下面实现一个轻量级发布订阅类。on 负责注册监听函数,off 负责移除,emit 负责触发并按顺序执行。为了保证触发过程顺畅,通常会把监听函数放进 try-catch 或者直接让错误向上抛出,便于定位问题。
class EventBus {
constructor() {
this.listeners = {};
}
on(event, handler) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(handler);
return this;
}
off(event, handler) {
if (!this.listeners[event]) return this;
const index = this.listeners[event].indexOf(handler);
if (index > -1) {
this.listeners[event].splice(index, 1);
}
return this;
}
emit(event, payload) {
if (!this.listeners[event]) return this;
this.listeners[event].forEach(function (handler) {
handler(payload);
});
return this;
}
}
const bus = new EventBus();
function onUserLogin(user) {
console.log('登录成功', user.name);
}
bus.on('login', onUserLogin);
bus.emit('login', { name: 'Alice' });
bus.off('login', onUserLogin);
观察者模式的优点是把发送者和接收者解耦,发送者不需要知道谁在监听。但滥用观察者模式会导致事件流难以追踪,一个事件的触发路径可能绕来绕去。尤其是页面上大量事件订阅却没有及时解绑时,还会造成内存泄漏。通常建议在组件卸载、页面销毁时集中调用 off 或使用一次性订阅 once。
三、组合实践:用单例加观察者实现一个轻量状态容器
很多前端状态管理库的底层思路都脱离不了这两个模式的组合。用一个单例保存全局状态,再用观察者模式在状态变化时通知所有订阅者。这样既保证了状态的唯一入口,又能让分散在各处的视图自动更新。下面的例子展示一个极简状态容器,它没有依赖任何框架,但已经具备了 store 的雏形。
class Store {
static instance = null;
constructor(initialState) {
this.state = initialState;
this.listeners = [];
}
static getInstance(initialState) {
if (!Store.instance) {
Store.instance = new Store(initialState);
}
return Store.instance;
}
getState() {
return this.state;
}
subscribe(listener) {
this.listeners.push(listener);
return function unsubscribe() {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
}.bind(this);
}
setState(nextState) {
const prevState = this.state;
this.state = Object.assign({}, prevState, nextState);
this.listeners.forEach(function (listener) {
listener(this.state, prevState);
}, this);
}
}
const store = Store.getInstance({ count: 0 });
const unsubscribe = store.subscribe(function (next, prev) {
console.log(next.count, prev.count);
});
store.setState({ count: 1 });
unsubscribe();
这个简易 Store 通过 getInstance 保证全局只有一个状态对象,通过 subscribe 注册更新函数,setState 负责合并状态并触发通知。可以看到,设计模式在这里并不是强制要求,而是很自然地落进了代码结构里。真实项目可以继续扩展中间件、异步 action、状态快照等能力,但核心思想不会变。
使用组合模式时需要注意的是,如果状态结构复杂,每次 setState 都全量通知会带来不必要的渲染。这时可以引入选择器或细粒度订阅,让监听函数只关心自己需要的字段,从而减少无意义更新。这也是在理解模式之后需要继续思考的边界问题。
四、其他常见设计模式与 JS 编程思想
除了单例和观察者,JavaScript 中还有很多模式与语言特性结合紧密。策略模式可以用对象映射替代频繁的 switch-case,把每种算法或分支独立成键值对;装饰器模式则利用高阶函数在运行时给函数增加额外行为,而不修改原函数。例如一个函数执行前后添加日志,就可以写成高阶函数包装。
function withLogging(fn) {
return function (...args) {
console.log('调用参数', args);
const result = fn.apply(this, args);
console.log('返回结果', result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
loggedAdd(2, 3);
这类模式背后的编程思想其实很一致:把容易变化的部分隔离在固定结构里,用组合代替继承,让模块职责更单一。设计模式不是死板的代码模板,而是针对特定问题的沟通词汇和结构经验。在 JavaScript 中由于闭包、高阶函数和对象字面量的存在,许多经典模式可以用更短小的代码实现,因此在写代码时更应该关注解决了什么问题,而不是它到底该叫哪个名字。
最后需要提醒的是,设计模式的价值在维护阶段才会充分显现。刚开始引入单例和观察者可能会觉得多了一些间接层,但当项目规模增长、多人协作时,清晰的实例约束和消息通路能显著降低沟通成本。建议结合实际业务逐步引入,避免在简单场景中过度设计。
JavaScript设计模式单例模式观察者模式修改时间:2026-09-30 08:14:13