如何在JavaScript开发中巧妙运用单例模式与观察者模式?

来源:IPIPP.com作者:沙月恵奈‌头衔:网络博主
导读:本期聚焦于沙月恵奈‌创作的《如何在JavaScript开发中巧妙运用单例模式与观察者模式?》,敬请观看详情。模块级状态被意外重置、组件间通知链路混乱、同一配置对象反复实例化,这些问题往往不是逻辑错误,而是对象创建和消息分发缺少约束。单例模式限制一个类只能产生一个实例,将共享状态收敛到唯一入口;观察者模式建立一对多的依赖网络,让状态变更能够自动触发多个订阅者。在 JavaScript 中,闭包和原型链让这两种模式实现起来比传统面向对象语言更轻量,事件监听、全局状态容器、弹窗管理、WebSocket 连接复用等场景都能看到它们的影子。本文围绕实现代码、应用边界和组合实践展开,帮助你理解设计模式背后的编程思想,而不是机械套用模板,同时也会补充策略模式和装饰器模式在 JavaScript 中的简捷落地方式。

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

如何在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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0930/63750.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。