首页
Preview

我最喜欢的9种JavaScript设计模式

设计模式是可重用的解决方案,用于解决软件开发过程中出现的常见问题。每个JavaScript程序员都遇到过与你相同的问题,并且同样的解决方案已经被反复使用。这些解决方案就是设计模式。

每种编程语言都有很多由其社区创建的这些解决方案。来自几个开发人员的这种共同经验使得设计模式非常有用。它们帮助我们编写优化的代码并解决问题。另一个巨大的优势是,由于它们如此普遍,不同的开发人员可以轻松理解彼此的代码。

设计模式的最大好处是:

  • **有效的解决方案:**由于它们被许多开发人员使用,因此你可以确信它们有效。不仅如此,而且由于这些模式已经被使用多次,进行了多次优化。
  • **易于重用:**按定义,设计模式是可重用的,即使它们非常通用,也可以轻松地适应特定问题。
  • **它们是表达性的:**设计模式可以用优雅的格式描述复杂的解决方案。
  • **减少了重构的需要:**当你以设计模式为重点编写应用程序时,更容易更快地获得干净的代码。这样,我们可以减少重构。特别是在JavaScript中,这种语言允许以许多方式编写相同的内容。
  • **使你的代码更小:**由于设计模式通常是经过优化的,因此需要更少的代码来实现,而更少的代码意味着更少的错误。

考虑到所有这些,你应该已经愿意开始在项目中使用设计模式了。我将简要介绍JavaScript的历史,概述一些重要特性,然后我们将更深入地了解设计模式。

JavaScript简史

现在,JavaScript是Web开发最流行的语言,但在它的诞生之初,它只是HTML元素之间的“胶水”。JavaScript被设计为Netscape浏览器的脚本语言。那时,浏览器能做的一切就是呈现静态HTML页面,JavaScript的出现导致了当时大公司之间的一场战争,Netscape和Microsoft。

20世纪90年代的大公司想使用自己的脚本语言,Netscape使用JavaScript(由Brendan Eich于1995年创建),而Microsoft使用JScript。

可以想象的是,它们之间存在许多差异,每个网站都必须针对特定浏览器完成,始终具有“最佳查看于...”的信号。很快就清楚了我们需要一个标准,一种跨浏览器的语言,以统一开发过程并简化Web页面的创建。结果就是ECMAScript

ECMAScript是一种脚本语言规范,所有浏览器都尝试支持它。有几种ECMAScript实现,但最流行的是JavaScript。自推出以来,ECMAScript已经标准化了许多重要的内容,对于好奇的人,可以在Wikipedia上找到列表。因此,当人们说像ES6这样的东西时,他们指的是JavaScript对ECMAScript规范版本6的实现。

JavaScript的一些重要特性

我们需要更详细地了解一些语言方面,以帮助我们实现设计模式。我们可以这样定义JavaScript:

JavaScript是一种轻量级、解释型、面向对象的语言,具有将函数作为一等公民的特点,最为人所知的是作为Web页面的语言。

这个定义是说JavaScript对系统内存的占用很小,易于使用,易于学习,并且具有与其他流行语言类似的语法。最初,JavaScript是一种解释型语言,但现在它使用JIT(即时)编译器。具有面向过程编程、面向对象编程和函数式编程的支持,使其非常灵活(也会导致许多问题)。

现在我们知道了JavaScript是什么以及它的主要特征,让我们看一些特性。

JavaScript支持一等函数

如果你来自像C或C ++这样的语言,那么这可能更难理解。说JavaScript将函数视为一等公民是指你可以将函数作为常规参数传递给其他函数。几乎就像它们是对象一样。

JavaScript基于原型

与任何其他面向对象语言一样,JavaScript支持对象,而当我们说对象时,我们立即想到类和继承。这就是事情变得奇怪的地方,最初JavaScript没有类的支持,并且仍然使用基于原型的继承。

基于原型的编程是一种通过重用已经存在的对象进行扩展而构建特性重用(继承)的样式,而不是实现它们。我们将在设计模式示例中更好地看到这一点。这个特性在许多模式中非常重要。

JavaScript上的事件循环

如果你曾经在JavaScript上编程过,那么肯定熟悉术语回调。对于那些不了解的人,回调是作为参数传递的函数,当事件被触发时将执行。它们通常用于侦听事件,例如鼠标单击或按钮按下。

每次触发具有侦听器的事件时,都会向队列发送一条消息,该队列以FIFO(先进先出)方式同步处理。这称为事件循环。

队列中的每个消息都与一个函数相关联。每当消息离开队列时,在处理任何其他消息之前,函数都会完全执行。这称为运行至完成。

queue.processNextMessage()以同步方式等待新消息。正在处理的每个消息都有自己的堆栈,并在堆栈为空之前进行处理。一旦所有进程都完成,就会从队列中读取新消息,这样继续进行,直到队列中有消息为止。

你可能还听说过JavaScript是非阻塞的。当处理异步操作时,程序可以继续处理其他内容,例如新输入,而不会阻塞主线程。这个JavaScript属性非常有用,事实上,这就是很多人选择在浏览器之外使用该语言的原因。这是一个非常有趣的话题,值得一篇文章来详细介绍。# 什么是设计模式?

设计模式是常用的解决软件开发问题的方案。

Proto-pattern

一个模式是如何被创建的?假设你发现了一个经常出现的问题,并且你有一个独特的解决方案,这个方案还没有在任何地方被记录过,甚至没有在 Stack Overflow 上出现过。你每次遇到这个问题都使用这个解决方案,并且认为它是可重用的,所有开发者都能从中受益。

你的解决方案立即成为一个模式吗?幸运的是,不是。对于一个有良好开发实践的人来说,很容易把看起来像模式的东西误认为是模式。

你如何知道你找到的东西是否真的是一个设计模式?

通过与其他开发者共享它,编程是一个由庞大社区组成的团队运动。每个模式在成为设计模式之前都必须经过一个阶段,即原型模式。

在成为真正的模式之前,原型模式需要被多个开发者使用和测试。为了让模式被社区认可和使用,需要进行大量的文档工作。

Anti-pattern

正如设计模式代表了良好实践,反模式则代表了不良实践。

在 JavaScript 中,一个很好的反模式示例是更改 Object 的原型。在 JavaScript 中,几乎所有对象都继承自 Object(请记住,JavaScript 使用基于原型的继承),因此想象一下,如果你更改了这个原型,那么所有继承自它的对象都会受到影响——这几乎是 JavaScript 中的所有对象。这是一个灾难等待发生的情况!

另一个例子是更改你不了解的对象。对一个在整个应用程序中广泛使用的对象的方法进行小的修改可能会造成巨大的混乱,团队越大,混乱就越大。你会遇到命名冲突、不兼容的实现以及维护的噩梦。如果运气好,你的测试可以在发生任何事情之前挽救你。

知道好的实践是很重要的,知道坏的实践也是个好主意。这是一个认识错误并在为时过早之前避免它们的好方法。

设计模式分类

设计模式可以按照多种方式进行分类,以下是最流行的几种:

  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 并发设计模式
  • 架构设计模式

创建型设计模式

这些模式处理对象创建,比基本对象创建方式更加优化。当常规对象创建方式会导致更多的复杂性或给代码带来问题时,创建型模式可以解决问题。

结构型设计模式

这些模式处理对象之间的关系。它们确保如果系统的某个部分发生变化,其他部分不必跟随变化。

行为型设计模式

这种模式认识、实现和改进系统中对象之间的通信。它们有助于确保应用程序的不相关部分具有同步信息。

并发设计模式

当你处理多线程编程时,这些模式是你想要使用的模式。

架构设计模式

这些设计模式用于系统架构,如 MVC 或 MVVM。

在下一节中,我们将查看一些这些模式的示例,以便更好地理解。

我最喜欢的9个设计模式示例

每个设计模式都代表了给定问题的解决方案。没有一个通用的模式集,可以解决你遇到的每个问题。我们需要了解何时给定模式将是有用的,以及它如何实际提供价值。一旦我们熟悉了设计模式及其适用情况,我们将能够确定我们可以使用哪个模式以及它是否解决了手头的问题。

记住,使用错误的模式可能会导致不必要的复杂性和性能损失。

这些都是在我们考虑将设计模式应用于我们的代码时需要考虑的重要事项。让我们看看一些在 JavaScript 中更有用的模式,每个资深 JavaScript 开发人员都应该知道。

1. 构造函数模式

当我们考虑面向对象语言的经典实现时,构造函数是一种特殊的函数,它以默认值或来自调用者的输入初始化类的值。

JavaScript 中创建对象的三种常见方法是:

创建对象后,我们有四种方法向其添加属性。它们是:

对象创建的最常见方式是使用 {},然后使用点符号或 [] 添加属性。这就是为什么我建议你使用这些方法,它会让其他程序员更容易理解你的代码,甚至是你未来的自己。

我们之前提到 JavaScript 不支持原生类,但它支持通过在函数调用前加上 new 前缀来使用构造函数。通过这种方式,我们可以将函数用作构造函数,并像在更传统的语言中一样初始化属性。

我们仍然可以改进这段代码。问题在于 writesCode 方法为每个 Person 实例重新定义。由于 JavaScript 是基于原型的,我们可以通过将方法添加到原型中来避免这种情况。

现在,所有 Person 的实例都可以访问相同的共享 writesCode() 实例。

就像在第一个示例中,每个类型为 Person 的对象都有一个 不同的方法,每个 Person 都指向自己的函数定义。在第二个示例中,所有 Person 将指向相同的函数,相同的代码片段。

2. 模块模式

尽管是面向对象的,但 JavaScript 以其自己的独特方式实现。因为它没有适当的类,所以它也无法限制对类组件的访问。在像 Java 或 C++ 这样的语言中,你可以定义类成员的访问权限(private、protected、public 等),但是作为一个聪明的小东西,JavaScript 有一种模拟此行为的方法。

在我们深入了解模块模式的细节之前,让我们更好地了解一下闭包。闭包是具有对其父作用域的访问权限的函数,即使父作用域已经完成。它们将帮助我们模拟访问限制器的行为。让我们看一个例子:

正如你所看到的,我们将变量 counter 绑定到一个被调用并关闭的函数上,但我们仍然可以通过增加计数器的子函数访问它。请注意,内部函数从 counterIncrementer() 返回。我们无法从函数外部访问计数器,本质上,我们通过作用域操作在原始 JavaScript 中创建了一个私有变量。

使用闭包,我们甚至可以创建具有公共和私有部分的对象。当我们希望隐藏对象的某些部分并仅向模块的用户展示漂亮的接口时,这非常有用。例如:

这种模式的最大效用在于在对象的公共和私有部分之间进行明确的分离。

并非一切都是幸福的,这种模式也有一些问题。当我们想要更改成员的可见性时,我们必须在每个调用者中更改它,因为对于公共和私有部分,访问权限是不同的。另一个问题是,在创建对象后添加方法无法访问私有方法(但我们不想添加新方法)。## 3. 揭示模块模式

这是上面描述的模块模式的一种演变。主要区别在于,我们在私有作用域中编写所有对象的逻辑,然后通过匿名对象公开我们想要的内容。我们还可以在将其映射到公共对象时更改私有成员的名称。

揭示模块模式是实现模块模式的方式之一。揭示模块模式与其他变体之间的差异主要在于我们引用公共模块的方式。因此,揭示模块模式更容易使用和更改。然而,在某些情况下,仍然可能会出现问题,例如当我们在继承链上使用对象时。最棘手的情况包括:

  • 如果我们有一个私有函数引用公共函数,我们无法覆盖公共函数。私有函数将继续指向私有实现,从而导致错误。
  • 当我们有一个公共成员指向私有成员,并且我们试图从模块外部覆盖公共成员时,所有其他函数仍将引用私有值,从而引入错误。

4. 单例模式

当我们想要允许类的单个实例时,我们使用单例模式。例如,当我们有一个配置对象时,我们不希望每次调用它时都创建一个新对象,它必须始终相同,否则我们可能每次都有不同的设置。

生成的随机数始终相同,就像config的值一样。

5. 观察者模式

当我们想要优化系统不同部分之间的通信时,观察者模式非常有用。它促进了各部分的集成,而不会使它们过于耦合。

你将找到几种不同的实现此模式的方式,但简单情况是当我们有一个发射器和大量观察者时。发射器将执行所有观察者订阅的操作。这些操作可以是订阅主题,从主题取消订阅以及每次发布主题时通知订阅者。

此模式的一个变体是发布/订阅模式,这是我们将在本文中看到的模式。

在观察者模式中,发射器保留对观察者的所有引用,并直接在这些对象上调用方法。另一方面,发布/订阅模式具有通道,这些通道充当发布者和订阅者之间的通信层。发布者触发事件并只执行发送到此事件的回调。

以下是发布/订阅模式的一个小例子。

当我们想要在不同的地方响应单个事件时,此设计模式特别有用。想象一下,你需要向API发出多个请求,并根据响应进行其他调用。你必须嵌套多个回调,这可能很难管理。使用发布/订阅模式,你可以以更简单的方式解决此问题。

这种模式的问题是测试。测试发布者和侦听器的行为可能很困难。

6. 中介者模式

在解耦系统中经常使用中介者。当系统的不同部分需要协调通信时,中介者可能是最佳选择。

与现实生活一样,中介者是将其他对象之间的通信聚焦点的对象。

乍一看,中介者和发布/订阅模式非常相似。实际上,它们都用于管理元素之间的通信。区别在于,发布/订阅模式将事件抛到空气中并忘记了它,而中介者将确保每个订阅者都处理消息。

中介者模式的一个很好的用例是向导。假设你在系统上有一个长的注册流程。通常,大型表单会分成几个步骤。

这是一种使代码维护更容易的方法,同时用户不会被巨大的表单所压倒。中介者可以管理整个向导,显示与每个步骤的输入相配合的不同步骤的使用。

这种模式的明显好处是改善系统各部分之间的通信。以同样的方式发生辩论,中介者确保每个人都有发言的时间,没有人乱说话。

中介者成为单点故障,如果它停止,一切都会停止。这是此模式的主要问题。

7. 原型模式

你可能已经厌倦了阅读此内容,但JavaScript不支持其本机形式的类。继承是通过原型完成的。

我们可以创建对象,这些对象将用作其他要创建的对象的原型。原型是构造函数制作的对象的蓝图。几乎像Java中区分类和对象一样。

让我们看一个此模式的示例。

请注意,通过原型继承最终会带来性能的改进,因为两个对象都引用在原型上实现的相同方法,而不是在每个对象上实现该方法。

8. 命令模式

当我们想要将调用封装为对象时,我们使用命令模式。这是一种将调用者上下文与被调用者分开的方法。

假设你的JavaScript应用程序有多个对API的调用。现在想象一下API发生了变化。我们不想更改与API交互的每个位置。

这就是分离调用API的对象和确定何时调用它的对象的抽象层的地方。这样,我们不需要对应用程序进行大量更改,因为我们只需在一个位置更改对API的调用。

此模式引起的问题是它创建了一个额外的抽象层,可能会影响应用程序的性能。重要的是要知道如何平衡性能和代码可读性。

9. 外观模式

当我们想要在公开显示和内部实现之间创建一个抽象层时,使用外观设计模式。当我们想要一个更简单的界面时使用它。

例如,DOM选择器的库(如JQuery、Dojo和D3)使用此模式。这些框架具有强大的选择器,使我们可以以非常简单的方式编写复杂的查询。例如jQuery(".parent .child div.span")似乎很简单,但在其下隐藏了一个复杂的查询逻辑。

在此处,每当我们在代码上创建一个抽象层时,我们可能会失去性能。大多数情况下,此损失无关紧要,但始终要考虑。

结论

设计模式是每个JavaScript开发人员的基本工具。通过使一切变得更简单和更易于理解,它们在维护代码时非常有帮助。

这篇文章已经非常长了,如果你想了解更多关于设计模式的知识,我建议阅读经典书籍《设计模式:可重用面向对象软件的基础》(四人组)以及更现代的《学习JavaScript设计模式》(Addy Osmani)。两本书都非常好,值得一读(链接不附属)。这是一篇很长的文章,如果你看到这里了,请在Medium上关注我,并考虑与你的朋友分享!

构建可组合的前端和后端

不要构建 Web 单体应用程序。使用 Bit 创建和组合解耦的软件组件 - 在你最喜欢的框架中,如 React 或 Node。使用强大而令人愉悦的开发体验构建可扩展和模块化的应用程序。

将你的团队带到 Bit Cloud 中,一起托管和协作组件,大大加速、扩展和标准化团队开发。从可组合的前端开始,如设计系统或微前端,或者探索可组合的后端。试一试 →

译自:https://blog.bitsrc.io/my-9-favorite-design-patterns-in-javascript-9d2a09651d08

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
一个人玩
先找到想要的,然后出发

评论(0)

添加评论