javascript闭包怎样实现观察者模式

闭包能实现观察者模式是因为它提供了私有且持久的变量存储,使得订阅者列表_subscribers被安全封装在函数作用域内,外部无法直接访问;2. subscribe、unsubscribe和notify方法通过闭包共享_subscribers数组,实现对观察者的增删查和通知;3. 每次调用createeventbus都会创建独立的闭包环境,保证多个实例间互不干扰;4. 实际使用中需注意内存泄漏问题,即组件销毁时应主动取消订阅以避免残留回调引用导致无法回收;5. 通知顺序依赖订阅顺序,若需优先级控制则需扩展逻辑;6. 单个观察者报错可能中断通知流程,因此需用try…catch包裹回调执行以确保其他观察者仍能收到消息;7. 在复杂场景下,手动管理大量事件和订阅者会变得困难,建议引入eventemitter或rxjs等专业库进行更高效的事件流管理。

javascript闭包怎样实现观察者模式

说实话,用JavaScript闭包来实现观察者模式,这事儿真挺巧妙的。它核心的思路就是,让一个函数(或者说,它返回的对象)能记住并管理它自己的一堆“听众”——那些对它的状态变化感兴趣的部分。这样,当它内部发生点什么事儿的时候,就能悄咪咪地通知到所有这些听众,而且这个听众列表还是它自己的私有财产,外面轻易动不了。

javascript闭包怎样实现观察者模式

要实现这套机制,我们通常会创建一个函数,这个函数内部维护一个数组来存放所有的观察者(也就是那些回调函数)。然后,它会暴露出几个方法:一个用来添加观察者(

subscribe

),一个用来移除观察者(

unsubscribe

),还有一个是关键的,用来遍历这个数组并调用所有观察者的回调(

notify

)。这些方法都共享并操作着那个私有的观察者数组,而这个数组之所以能一直存在并被访问,正是闭包的功劳。

为什么说闭包是实现观察者模式的“自然”选择?

我个人觉得,闭包和观察者模式简直是天作之合。你想啊,观察者模式的核心不就是“一个主体(发布者)要管理一堆订阅者,并在特定事件发生时通知它们”吗?这里面最关键的一点,就是那个订阅者列表,它必须是主体的“私有财产”,不能随便被外部篡改,同时还得是持久化的,不能每次调用都重新初始化。闭包天然就提供了这种私有性和持久性。它把

observers

数组包裹在自己的作用域里,外部代码无法直接访问或修改它,只能通过你暴露出来的

subscribe

unsubscribe

方法来间接操作。这种封装性,让整个模式的实现显得特别干净、安全,也符合我们对模块化、解耦的追求。你不需要担心哪个外部代码不小心把你的订阅者列表给清空了,或者加了些不该加的东西。它就是那么稳妥地待在那里,等待被调用。

立即进入“豆包AI人工智官网入口”;

立即学习“豆包AI人工智能在线问答入口”;

javascript闭包怎样实现观察者模式

闭包实现观察者模式的具体代码示例与解析

来,我们直接看段代码,这样更直观。假设我们要创建一个简单的事件发布器:

function createEventBus() {    let _subscribers = []; // 这是一个私有变量,通过闭包保持持久性    return {        /**         * 订阅事件         * @param {Function} callback - 观察者回调函数         * @returns {Function} - 返回一个取消订阅的函数,方便链式调用或直接取消         */        subscribe: function(callback) {            if (typeof callback !== 'function') {                console.warn('订阅者必须是一个函数!');                return;            }            _subscribers.push(callback);            console.log('有新的订阅者加入!当前订阅数:', _subscribers.length);            // 返回一个取消订阅的函数,这是个很实用的模式            // 注意:这里的 `this` 指向返回的对象,确保 unsubscribe 正确调用            return () => {                this.unsubscribe(callback);            };        },        /**         * 取消订阅         * @param {Function} callback - 要移除的观察者回调函数         */        unsubscribe: function(callback) {            _subscribers = _subscribers.filter(sub => sub !== callback);            console.log('有订阅者离开了。当前订阅数:', _subscribers.length);        },        /**         * 通知所有订阅者         * @param {*} data - 要传递给观察者的数据         */        notify: function(data) {            console.log('开始通知所有订阅者...');            _subscribers.forEach(callback => {                try {                    callback(data);                } catch (error) {                    // 实际项目中这里可能需要更复杂的错误处理,比如记录日志                    console.error('通知某个订阅者时出错:', error);                }            });            console.log('通知完成。');        }    };}// 使用示例:const myEventBus = createEventBus();// 观察者1const observer1 = (message) => {    console.log('观察者1 收到消息:', message);};// 观察者2const observer2 = (message) => {    console.log('观察者2 收到消息:', message.toUpperCase());};// 订阅const unsubscribe1 = myEventBus.subscribe(observer1);myEventBus.subscribe(observer2);myEventBus.subscribe((data) => { // 匿名函数也可以    console.log('匿名观察者收到:', data.length, '个字符');});console.log('n--- 第一次通知 ---');myEventBus.notify('Hello World');// 取消订阅一个观察者unsubscribe1(); // 或者 myEventBus.unsubscribe(observer1);console.log('n--- 第二次通知 ---');myEventBus.notify('Goodbye');// 尝试订阅一个非函数myEventBus.subscribe('not a function');

这段代码里,

_subscribers

数组就是被

createEventBus

函数的闭包“捕获”的。每次调用

createEventBus()

都会创建一个新的、独立的

_subscribers

数组,以及一套操作这个数组的方法。这意味着,你创建的每个

myEventBus

实例都有自己独立的订阅者列表,互不干扰。

subscribe

unsubscribe

notify

这三个方法,无论何时被调用,都能访问到并操作它们共同作用域里的那个

_subscribers

数组,即使

createEventBus

函数本身已经执行完毕,它的作用域也因为闭包的存在而没有被销毁。这就是闭包在这里的核心魔力。

javascript闭包怎样实现观察者模式

这种实现方式有哪些潜在的挑战或需要注意的地方?

虽然闭包实现观察者模式很优雅,但实际使用中还是有些坑或者说需要注意的地方:

内存泄漏风险(不取消订阅):这是最常见的。如果一个组件订阅了某个事件,但在它被销毁时没有取消订阅,那么即使组件的DOM元素被移除了,它的回调函数仍然存在于发布者的

_subscribers

列表中。这意味着,这个回调函数以及它闭包引用的所有变量(包括旧的DOM元素等)都无法被垃圾回收,导致内存泄漏。所以,养成好习惯,哪里订阅了,哪里就得考虑取消订阅。比如在React的

useEffect

清理函数里,或者Vue的

beforeDestroy

钩子里。通知顺序不确定性:我们这个简单的

forEach

循环通知,是按照订阅的先后顺序来执行的。但在某些复杂场景下,你可能需要特定的通知顺序(比如优先级高的先执行),那这个简单的实现就不够了,需要额外逻辑来管理订阅者的顺序。错误处理:如果某个观察者的回调函数在执行时抛出了错误,默认情况下,这个错误可能会中断整个

notify

循环,导致后续的观察者无法收到通知。像我上面代码里加了个

try...catch

,这是个基本的防御,但在生产环境,你可能需要更健壮的错误日志和处理机制,比如即使某个观察者报错,也要确保其他观察者能正常收到通知。“僵尸”观察者问题:这其实是内存泄漏的一种表现。如果一个观察者对象本身已经被销毁,但它的回调函数还在订阅列表中,当发布者尝试通知它时,可能会因为访问不到它内部的某些状态而报错。虽然JavaScript的垃圾回收机制在这方面比C++之类的语言友好很多,但概念上还是值得注意。复杂场景下的管理:对于只有少数几个事件和观察者的简单应用来说,这种模式很好用。但如果你的应用有几十上百种事件,每个事件又有大量的订阅者,那么手动管理这些

subscribe

/

unsubscribe

调用就会变得非常繁琐且容易出错。这时候,你可能需要引入更高级的事件库(比如EventEmitter,或者RxJS这样的响应式编程库),它们提供了更强大的事件管理、流控制和错误处理能力。

以上就是javascript闭包怎样实现观察者模式的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/112713.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月23日 23:00:39
下一篇 2025年11月23日 23:24:06

相关推荐

  • Go语言中实现包级Logger的初始化与全局使用

    在go语言中,为了在`main`函数之外的多个功能模块中统一使用日志记录器(如`lumber`),避免重复声明,最佳实践是将其声明为包级变量。在`main`函数或`init`函数中进行一次性初始化后,该日志实例即可在整个包内被访问和调用,从而实现全局日志的统一管理和便捷使用。 背景与挑战 在Go应用…

    2025年12月16日
    000
  • Go语言在Windows环境下清空控制台的实用方法

    本文详细介绍了在go语言中如何在windows操作系统下清空控制台的有效方法。通过利用`os/exec`包执行系统命令,我们能够精确地调用windows的`cmd.exe`并传递`/c cls`参数来实现控制台的刷新。文章提供了完整的代码示例,并解释了该方法的原理,同时强调了其windows平台特异…

    2025年12月16日
    000
  • Golang如何开发简单的问卷调查项目

    答案是使用Golang搭建一个简易问卷系统,通过定义Survey和Response结构体,实现展示问卷、提交回答和查看结果的完整流程。 用Golang开发一个简单的问卷调查项目,核心是搭建HTTP服务、设计数据结构、处理表单提交和展示结果。整个过程不复杂,适合初学者练手。以下是具体实现思路和步骤。 …

    2025年12月16日
    000
  • Go语言中big.Int到指定进制字符串的转换方法与非导出函数探究

    本文深入探讨了在go语言中将`big.int`类型转换为自定义进制字符串的实践方法,特别是如何避免标准库`base32`包的额外格式。针对用户希望访问`math/big`包中非导出函数`nat.string`的需求,文章阐明了go语言中非导出函数无法直接访问的限制,并提出通过`strconv.for…

    2025年12月16日
    000
  • Go 项目测试文件组织:子目录、递归执行与覆盖率实践

    本文深入探讨 go 语言项目中测试文件的组织策略,重点介绍如何在子目录中管理测试、如何使用 `go test ./…` 命令进行递归测试,并分析其对包内容访问权限的影响。此外,文章还详细阐述了 go 1.20 引入的集成测试覆盖率功能,以及 `package_test` 模式的应用,旨在…

    2025年12月16日
    000
  • Go语言命名返回值:原理、用法与最佳实践

    go语言的命名返回值提供了一种声明函数返回变量的便捷方式,允许通过空 `return` 语句隐式返回这些变量的当前值,或通过显式 `return` 语句覆盖它们。这种机制得益于go在栈上分配参数和返回值的底层实现,使得命名返回值成为函数签名中预定义存储位置的自然表达。理解其工作原理有助于编写更清晰、…

    2025年12月16日
    000
  • Go语言中io.Writer接口的正确初始化与使用

    本文深入探讨了go语言中`io.writer`接口未初始化导致的运行时错误(nil指针解引用)问题。通过分析接口的本质及其与具体实现的关联,文章展示了如何正确地声明和初始化`io.writer`变量,并提供了使用`os.stdout`和`bytes.buffer`等具体类型进行初始化的示例,旨在帮助…

    2025年12月16日
    000
  • Go 语言命名返回值:用法、原理与最佳实践

    go 语言的命名返回值是一项强大特性,它允许在函数签名中声明返回变量,从而简化代码并提高可读性。本文深入探讨了命名返回变量的用法,包括其隐式和显式返回机制,并通过解释 go 函数参数和返回值在栈上的分配原理,揭示了其底层工作方式。我们将通过示例代码和汇编分析,确认其使用的合法性与高效性,并提供实践建…

    2025年12月16日
    000
  • 理解Go语言中io.Writer接口的空指针运行时错误

    本文深入探讨了Go语言中因未初始化`io.Writer`接口而导致的“panic: runtime error: invalid memory address or nil pointer dereference”运行时错误。文章通过分析示例代码,阐明了Go接口的零值行为,并提供了使用`os.Std…

    2025年12月16日
    000
  • 深入理解Go语言中io.Writer接口的nil值与运行时错误

    在Go语言中,未初始化的`io.Writer`接口变量默认为`nil`,当尝试对其执行写入操作时,会导致“panic: runtime error: invalid memory address or nil pointer dereference”运行时错误。本文将详细解释这一现象的原因,并通过示…

    2025年12月16日
    000
  • Go语言中io.Writer接口的正确初始化与使用:避免运行时错误

    本文详细解析了go语言中`io.writer`接口因未初始化而导致`nil`指针解引用运行时错误的原因。通过对比接口与具体类型的概念,并提供`os.stdout`和`bytes.buffer`等具体实现示例,指导开发者如何正确初始化并使用`io.writer`接口,从而避免常见的`panic`问题,…

    2025年12月16日
    000
  • Golang如何处理并发goroutine中的错误

    使用通道传递错误是Go并发中处理goroutine错误的核心方法,通过创建error类型通道让worker发送错误,主协程接收并处理。示例中doWork函数模拟出错,worker通过errCh发送错误,主函数读取并记录。采用缓冲通道可避免发送阻塞,尤其在多个worker场景下,主程序可等待所有完成后…

    2025年12月16日
    000
  • Go语言并发与锁机制的测试策略与最佳实践

    本文深入探讨了go语言中并发与锁机制测试的挑战与有效策略。它强调了传统日志驱动测试的局限性,并推荐利用go的`testing`包、`sync.waitgroup`和通道(channels)来自动化测试并发操作的顺序和阻塞行为。文章还进一步倡导采用go的csp(communicating sequen…

    2025年12月16日
    000
  • Go cgo在ARM平台上编译C标准库头文件问题解析与解决

    本文旨在解决go语言项目在arm架构(如树莓派)上使用cgo编译时,因找不到c标准库头文件(如`math.h`)而导致的构建失败问题。核心在于理解cgo的编译机制,并正确配置`// #cgo cflags`指令以指定c编译器头文件搜索路径,以及使用`// #cgo ldflags`链接必要的c库,避…

    2025年12月16日
    000
  • 解决Go Cgo在ARM平台编译时无法找到C标准库头文件的问题

    本文旨在解决go语言项目在arm架构(如raspberry pi)上使用cgo编译时,因c标准库头文件缺失而导致的编译失败问题。我们将详细介绍正确的cgo指令语法、cflags与ldflags的区别及使用场景,并提供具体示例,确保go与c代码的无缝集成编译。 在Go语言项目中集成C代码时,cgo 是…

    2025年12月16日
    000
  • 解决Go CGO项目在ARM平台编译时C标准库找不到的问题

    本文旨在解决go语言使用cgo在raspberry pi等arm架构平台编译时,c代码无法找到标准库头文件(如`math.h`)的问题。核心在于正确配置go源文件中的`// #cgo`指令,特别是注意其语法规范、`cflags`用于指定头文件路径以及`ldflags`用于链接必要的c库,确保跨平台编…

    2025年12月16日
    000
  • Go语言并发编程:构建安全高效的通道复用器

    本文深入探讨了在go语言中实现通道复用器(channel multiplexer)的常见陷阱与最佳实践。通过分析一个初始实现中存在的闭包变量捕获问题和竞态条件,文章详细阐述了如何利用函数参数传递和`sync.waitgroup`来构建一个健壮、高效且能公平处理多个输入通道的复用器。 理解通道复用器 …

    2025年12月16日
    000
  • Go语言并发编程:构建安全高效的通道多路复用器

    本文深入探讨了go语言中如何实现一个安全高效的通道多路复用器(channel multiplexer)。我们将从一个常见的初学者错误入手,详细解析go协程中闭包变量捕获问题以及共享状态下的并发安全隐患,并展示如何利用`sync.waitgroup`和正确的变量传递机制来构建一个健壮的通道合并方案,确…

    2025年12月16日
    000
  • Go 语言中 ^0 的含义及其应用解析

    `^0` 在 go 语言中表示对零进行位补码运算。在大多数采用二进制补码表示负数的系统中,`^0` 的结果是 `-1`。本文将深入解析 `^0` 的位运算原理、它在 go 语言中的具体行为,并通过示例代码展示其常见应用场景,帮助开发者理解并正确使用这一特殊操作符。 ^ 运算符:位补码的基础 在 Go…

    2025年12月16日
    000
  • Python中模拟Go语言的Channel Select机制

    本文深入探讨了go语言中`select`语句的强大并发通信能力,并详细阐述了如何在python环境中,利用`threading`模块和`queue`数据结构,构建一个功能类似的通道选择机制。通过创建独立的监听线程将多个源队列的消息汇集到一个中心队列,python程序能够有效地等待并处理来自不同并发源…

    2025年12月16日
    000

发表回复

登录后才能评论
关注微信