JavaScript 发布/订阅模式实现与面试解析
本文深入解析JavaScript中的发布/订阅(观察者)模式,阐述其通过松散耦合促进对象间通信的核心思想。详细讲解发布者的核心成员结构,包括订阅、取消订阅及发布消息的方法实现。结合面试中关于参数传递与方法注册的讨论,提供基于对象字面量和ES6 Class的完整代码示例,帮助开发者掌握该设计模式的实际应用。
本文详细介绍JavaScript中发布/订阅模式的实现原理与应用方法,旨在为读者提供清晰的参考与学习思路。
发布/订阅模式简介
一、发布/订阅模式简介
发布/订阅模式,也称为观察者模式,其核心设计动力在于促进系统组件间的松散耦合。在该模式下,对象之间不再直接调用彼此的方法,而是由订阅者注册对发布者特定活动的关注。当发布者的状态发生变化时,所有已注册的订阅者都会收到通知。其中,接收通知的对象被称为订阅者或观察者,而发出通知的对象则被称为发布者或主题。当重要事件发生时,发布者会遍历并调用所有订阅者的回调函数,通常通过事件对象传递相关消息。
基本实现思路如下:发布者对象需维护一个数组类型的属性,用于存储所有已注册的订阅者。所谓的“订阅”操作,即将新的订阅者添加到该数组中;而“注销”则是从数组中移除特定的订阅者。此外,“发布”消息的操作即遍历订阅者列表,并依次调用它们注册时提供的方法以完成通知。
发布订阅方法的实现讨论
二、如何发布订阅者的方法?
上述基本思路大体正确。但在实际面试场景中,我曾提出另一种方案:在发布者之外定义一个新的订阅者类,并在该类中定义类似 getNews 的方法,以便在发布者发布消息时调用该方法。然而,面试官指出这种方式过于繁琐,且存在隐患:如果订阅者对象中不存在该方法,程序将报错。
随后,我尝试在发布消息时直接传递参数:obj.news = msg;。面试官对此表示质疑,认为这同样增加了复杂性,因为需要额外判断订阅者对象是否具备 news 属性,否则会导致 undefined 报错。
面对这一困境,面试官给出了关键建议:“可以使用继承机制,或者在注册订阅时直接传入一个函数。”
面试结束后,我查阅了相关资料并整理了以下关键注意点:
- 发送消息(即通知)的本质是调用订阅者对象的特定方法。因此,当用户执行订阅操作时,必须向发布者的 subscribe() 方法提供一个具体的函数引用。这正是面试官所指的“在注册时传入一个方法”的核心含义。
- 每个标准的发布者对象应包含以下核心成员:
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程序设计有所帮助。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
