JavaScript 发布/订阅模式实现与面试解析

作者:函数茶馆

时间:2026-10-22

浏览:393

本文深入解析JavaScript中的发布/订阅(观察者)模式,阐述其通过松散耦合促进对象间通信的核心思想。详细讲解发布者的核心成员结构,包括订阅、取消订阅及发布消息的方法实现。结合面试中关于参数传递与方法注册的讨论,提供基于对象字面量和ES6 Class的完整代码示例,帮助开发者掌握该设计模式的实际应用。

文章目录

本文详细介绍JavaScript中发布/订阅模式的实现原理与应用方法,旨在为读者提供清晰的参考与学习思路。

发布/订阅模式简介

一、发布/订阅模式简介

发布/订阅模式,也称为观察者模式,其核心设计动力在于促进系统组件间的松散耦合。在该模式下,对象之间不再直接调用彼此的方法,而是由订阅者注册对发布者特定活动的关注。当发布者的状态发生变化时,所有已注册的订阅者都会收到通知。其中,接收通知的对象被称为订阅者或观察者,而发出通知的对象则被称为发布者或主题。当重要事件发生时,发布者会遍历并调用所有订阅者的回调函数,通常通过事件对象传递相关消息。

基本实现思路如下:发布者对象需维护一个数组类型的属性,用于存储所有已注册的订阅者。所谓的“订阅”操作,即将新的订阅者添加到该数组中;而“注销”则是从数组中移除特定的订阅者。此外,“发布”消息的操作即遍历订阅者列表,并依次调用它们注册时提供的方法以完成通知。

发布订阅方法的实现讨论

二、如何发布订阅者的方法?

上述基本思路大体正确。但在实际面试场景中,我曾提出另一种方案:在发布者之外定义一个新的订阅者类,并在该类中定义类似 getNews 的方法,以便在发布者发布消息时调用该方法。然而,面试官指出这种方式过于繁琐,且存在隐患:如果订阅者对象中不存在该方法,程序将报错。

随后,我尝试在发布消息时直接传递参数:obj.news = msg;。面试官对此表示质疑,认为这同样增加了复杂性,因为需要额外判断订阅者对象是否具备 news 属性,否则会导致 undefined 报错。

面对这一困境,面试官给出了关键建议:“可以使用继承机制,或者在注册订阅时直接传入一个函数。”

面试结束后,我查阅了相关资料并整理了以下关键注意点:

  1. 发送消息(即通知)的本质是调用订阅者对象的特定方法。因此,当用户执行订阅操作时,必须向发布者的 subscribe() 方法提供一个具体的函数引用。这正是面试官所指的“在注册时传入一个方法”的核心含义。
  2. 每个标准的发布者对象应包含以下核心成员:

subscribers:一个数组,用于存储所有订阅者; subscribe():注册/订阅方法,负责将订阅者添加到 subscribers 数组中; unsubscribe():取消订阅方法,用于从 subscribers 数组中移除指定的订阅者; publish():发布方法,负责循环遍历 subscribers 数组中的每一个元素,并调用它们注册时所提供的回调函数; 上述所有方法均需接受一个 type 参数。这是因为发布者可能同时触发多种类型的事件(例如同时发布杂志和报纸),而订阅者可以选择性地只订阅其中某一种类型,忽略其他类型。

代码实现

三、代码实现

参考《JavaScript 模式》一书,使用对象字面量方式实现的代码如下:


// 由于这些成员对于任何发布者对象都是通用的,故将它们作为独立对象的一个部分来实现是很有意义的。那样我们可将其复制到任何对象中,并将任意给定对象变成一个发布者。
// 如下实现一个通用发布者,定义发布者对象……
let publisher = {
 subscribers: {
  any: []
 },
 subscribe: function (fn, type = `any`) {
  if (typeof this.subscribers[type] === `undefined`) {
   this.subscribers[type] = [];
  }
  this.subscribers[type].push(fn);
 },
 unSubscribe: function (fn, type = `any`) {
  let newSubscribers = [];
  this.subscribers[type].forEach((item, i) => {
   if (item !== fn) {
    newSubscribers.push(fn);
   }
  });
  this.subscribers[type] = newSubscribers;
 },
 publish: function (args, type = `any`) {
  this.subscribers[type].forEach((item, i) => {
   item(args);
  });
 }
};
// 定义一个函数makePublisher(),它接受一个对象作为参数,通过把上述通用发布者的方法复制到该对象中,从而将其转换为一个发布者
function makePublisher(obj) {
 for (let i in publisher) {
  if (publisher.hasOwnProperty(i) && typeof publisher[i] === `function`) {
   obj[i] = publisher[i];
  }
 }
 obj.subscribers = { any: [] };
}
// 实现paper对象
var paper = {
 daily: function () {
  this.publish(`big news today!`);
 },
 monthly: function () {
  this.publish(`interesting analysis`, `monthly`);
 }
};
// 将paper构造成一个发布者
makePublisher(paper);
// 看看订阅对象joe,该对象有两个方法:
var joe = {
 drinkCoffee: function (paper) {
  console.log(`Just read ` + paper);
 },
 sundayPreNap: function (monthly) {
  console.log(`About to fall asleep reading this ` + monthly);
 }
};
// paper注册joe(即joe向paper订阅)
paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, `monthly`);
// 即joe为默认“any”事件提供了一个可被调用的方法,而另一个可被调用的方法则用于当“monthly”类型的事件发生时的情况。现在让我们来触发一些事件:
paper.daily();   // Just read big news today
paper.daily();   // Just read big news today
paper.monthly();  // About to fall asleep reading this interesting analysis
paper.monthly();  // About to fall asleep reading this interesting analysis
paper.monthly();  // About to fall asleep reading this interesting analysis

此外,我还尝试使用 ES6 的 class 语法重新实现了该模式(注:此部分为转载者自行编写,原文作者使用的是函数方式实现):


// 由于这些成员对于任何发布者对象都是通用的,故将它们作为独立对象的一个部分来实现是很有意义的。那样我们可将其复制到任何对象中,并将任意给定对象变成一个发布者。
// 如下实现一个通用发布者,定义发布者对象……
class Publisher {
 constructor(){
  this.subscribers = {
   any: []
  }
 }
 subscribe(fn, type=`any`){
  if(typeof this.subscribers[type] === `undefined`){
   this.subscribers[type] = [];
  }
  this.subscribers[type].push(fn);
 }
 unSubscribe(fn, type=`any`){
  let newArr = [];
  this.subscribers[type].forEach((item, i) => {
   if(item !== fn){
    newArr.push(fn);
   }
  });
  this.subscribers[type] = newArr;
 }
 publish(args, type=`any`){
  this.subscribers[type].forEach((item, i) => {
   item(args);
  });
 }
 // 定义一个函数makePublisher(),它接受一个对象作为参数,通过把上述通用发布者的方法复制到该对象中,从而将其转换为一个发布者
 static makePublisher(obj){
  obj.publisher = new Publisher();
 }
}
// 实现person对象
var person = {
 sayHi: function(name){
  this.publisher.publish(name);
 },
 sayAge: function(num){
  this.publisher.publish(num, `age`);
 }
}
// 将person构造成一个发布者
Publisher.makePublisher(person);
// 看看订阅对象myLover,该对象有两个方法:
var myLover = {
 name: ``,
 hello: function(name){
  this.name = name;
  console.log(`Hi, i am ` + name + ` ! Nice to meet you!`);
 },
 timeOfLife: function(num){
  console.log(`Hello! My name is ` + this.name + ` ! I am ` + num + ` years old!`);
 }
}
// person注册myLover(即myLover向person订阅)
person.publisher.subscribe(myLover.hello);
person.publisher.subscribe(myLover.timeOfLife, `age`);
// 即myLover为默认“any”事件提供了一个可被调用的方法,而另一个可被调用的方法则用于当“age”类型的事件发生时的情况。现在让我们来触发一些事件:
person.sayHi(`Jimmy`);  // Hi, i am Jimmy ! Nice to meet you!
person.sayAge(24);    // Hello! My name is Jimmy ! I am 24 years old!
person.sayHi(`Tom`);   // Hi, i am Tom ! Nice to meet you!
person.sayAge(6);     // Hello! My name is Tom ! I am 6 years old!
person.sayAge(18);    // Hello! My name is Tom ! I am 18 years old!

更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章