前言
数组遍历方法和设计模式是JavaScript开发中的核心基础。带你理解常用数组方法(forEach、map、filter等)的使用以及发布订阅和观察者模式的实现。
基础知识:
数组遍历方法
forEach:用于遍历数组,接收回调函数,回调参数包括当前元素、下标、整个数组。
map:与forEach类似,但会返回一个新数组,新数组的元素由回调函数返回值组成。
filter:筛选符合条件的元素,返回新数组。
find:查找符合条件的元素,返回第一个匹配项。
findIndex:从左往右查找符合条件的第一个元素下标,返回第一个匹配项的索引。
includes:判断数组是否包含某个元素,返回布尔值。
indexOf:查找元素下标,找到就返回下标,若不存在返回-1。
发布订阅
-
发布订阅模式是一种设计模式,它定义了一种一对多的关系,让多个订阅者同时监听某一个发布者的状态,当发布者的状态发生改变时, 会通知所有的订阅者,使他们能够执行各自的操作,订阅者会根据发布者的状态来执行不同的操作。
-
发布者不直接接触订阅者,而是通过一个中间对象(第三方进行通信)来完成,这个中间对象负责管理订阅者,并通知它们状态改变。
class EventEmitter {
constructor() {
this.eventList = {
// 'hasHouse': [kang, wrap],
// 'hasCar': [cheng]
};
}
on(eventName, cb) {
if (!this.eventList[eventName]) {
this.eventList[eventName] = [];
}
this.eventList[eventName].push(cb);
}
emit(eventName) {
if (this.eventList[eventName]) {
const handlers = this.eventList[eventName].slice(); //
handlers.forEach((item) => {
item();
});
}
}
off(eventName, cb) {
const callbacks = this.eventList[eventName];
const index = callbacks.indexOf(cb);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
once(eventName, cb) {
const wrap = () => {
cb();
this.off(eventName, wrap);
};
this.on(eventName, wrap);
}
}
function p1() {
console.log("p1");
}
function p2() {
console.log("p2");
}
function p3() {
console.log("p3");
}
let eventEmitter = new EventEmitter();
eventEmitter.on("buy food", p1);
eventEmitter.off("buy food", p1);
eventEmitter.on("buy food", p2);
eventEmitter.on("buy food", p3);
eventEmitter.on("buy House", p3);
eventEmitter.emit("buy food");
eventEmitter.emit("buy House");
-
发布订阅设计模式详解
- 发布订阅模式是一种一对多的设计模式,多个观察者可以同时监听一个主题对象,当主题对象状态发生变化时,所有订阅者都会收到通知并执行各自的操作。
- 现实生活中的例子包括微信群通知、微博关注、朋友圈订阅等,都属于发布订阅模式的应用。
- 编程世界中的发布订阅模式常用于事件监听机制,如按钮点击事件、数据变更通知等。
-
发布订阅模式的代码实现
- 实现发布订阅模式需要构建一个类,包含两个核心方法:
on(用于订阅)和emit(用于发布)。 on方法用于注册事件和对应的回调函数,emit方法用于触发事件并执行所有订阅者的回调函数。- 在类中使用对象(哈希表)存储事件和回调函数,确保相同事件的多个订阅者可以被统一管理。
- 每次调用
on时,将回调函数存入对应事件的数组中;调用emit时,取出数组并依次执行回调函数。 - 支持取消订阅(
off),通过indexOf查找回调函数并从队列中移除。 - 支持单次订阅(once),在回调函数执行后自动取消订阅。
- 实现发布订阅模式需要构建一个类,包含两个核心方法:
需要注意的是这里的类,你把类看成是一个构造函数,写在construct里面的东西,就等同于是写在这个构造函数里面的东西。写在这个constructor外面的东西,但还是在类里面的,那就等同于是往这个构造函数的原型上面添加的东西。
观察者模式
- 发布者直接接触订阅者,订阅者会根据发布者的状态来执行不同的操作。 被观察者从观察者处订阅事件 观察者 =(通知变更)=> 被观察者
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>1</h2>
<button>点击</button>
<script>
const h2 = document.querySelector('h2')
const btn = document.querySelector('button')
let obj = {
count: 1
}
let num = obj.count // 防止递归报栈
function observer(val) {
// 找出页面上所有 用到了 count 这个变量的dom 结构 -- 找到订阅者
// 将订阅者的内容替换为新的值
h2.innerHTML = val
}
Object.defineProperty(obj, 'count', {
get() {
return num
},
set(val) {
observer(val)
num = val
}
})
btn.addEventListener('click', () => {
obj.count++
})
</script>
</body>
</html>
-
劫持函数的基本原理
通过Object.defineProperty劫持对象属性,可以控制属性的读取(get)、赋值(set)、可配置性(configurable)等行为。当属性被劫持后,其值的访问和修改必须经过劫持函数定义的get和set方法。 -
不可配置属性(configurable: false)
如果属性被设置为不可配置,则无法通过delete删除该属性,也无法修改其属性描述符,从而实现属性值的锁定。 -
get和set的作用
get用于控制属性值的读取,每次访问属性时都会触发get函数。set用于控制属性值的设置,每次对属性赋值时都会触发set函数,并接收到新的值。
-
递归调用导致的栈溢出问题
在set函数内部直接操作被劫持的属性(如obj.prop = value)会再次触发set,导致无限递归并最终栈溢出。解决方法是引入一个中间变量来保存实际值,避免直接操作被劫持属性。 -
劫持函数在数据绑定中的应用
通过劫持数据属性并结合DOM更新逻辑,实现数据变化自动更新视图的效果。例如:修改一个被劫持的count属性后,所有依赖该属性的DOM节点内容都会自动更新。 -
观察者模式的核心思想
观察者模式通过劫持属性值并监听其变化,触发视图更新。当数据变化时,通知所有依赖该数据的视图进行更新。例如:点击按钮修改count值后,所有使用该值的DOM元素内容都会自动更新。 -
发布订阅模式与观察者模式的区别
- 发布订阅模式中,发布者和订阅者之间通过一个中间层(事件中心)进行通信,彼此解耦。
- 观察者模式中,观察者直接监听被观察对象的变化,没有中间层,耦合度相对更高。
-
观察者模式的实际应用
可用于实现类似React中状态更新自动刷新视图的功能。当数据变更时,所有依赖该数据的组件都会自动更新。总结
-
模式实现要点:
- 发布订阅:第三方调度、事件解耦
- 观察者:直接监听、状态驱动
-
响应式原理:
Object.defineProperty的get/set机制