设计模式是在软件开发过程中解决常见问题的可重复使用的解决方案。每个 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 标准化了许多重要的东西 - 对于好奇的人,维基百科上有一份清单。因此,当人们说像 ES6 这样的话时,他们指的是 JavaScript 对 ECMAScript 规范版本 6 的实现。
JavaScript 的一些重要特性
我们需要仔细查看一些有助于我们实现设计模式的语言方面。我们可以这样定义 JavaScript:
JavaScript 是一种轻量级的语言,解释性的,面向对象的,函数是一等公民,通常被称为 Web 页面的语言。
这个定义是说 JavaScript 对系统内存的占用很小,易于使用,易于学习,并且具有类似于其他流行语言的语法。最初 JavaScript 是一种解释性语言,但现在它使用了即时编译器(JIT)。它支持过程式编程、面向对象编程和函数式编程,使其非常灵活(也会导致很多问题)。
现在我们知道了 JavaScript 是什么以及它的主要特征,让我们看看一些特性。
JavaScript 支持一等函数
如果你来自像 C 或 C++ 这样的语言,这可能更难理解。说 JavaScript 将函数视为一等公民是说你可以将函数作为普通参数传递给其他函数。几乎就像它们是对象一样。
JavaScript 基于原型
像任何其他面向对象的语言一样,JavaScript 支持对象,当我们说对象时,我们立即想到类和继承。这就是事情变得奇怪的地方,最初 JavaScript 没有支持类,仍然使用基于原型的继承。
基于原型的编程是一种通过重用已经存在的对象来扩展而不是实现特征(继承)的风格。我们将在设计模式示例中更好地了解这一点。这个特性对许多模式来说非常重要。
JavaScript 中的事件循环
如果你曾经在 JavaScript 中编程,那么肯定熟悉术语 callback。对于那些不熟悉的人,回调是作为参数传递的函数,当事件触发时将执行它。它们通常用于监听事件,如鼠标单击或按钮按下。
每次触发具有监听器的事件时,都会将消息发送到一个队列中,该队列按照先进先出(FIFO)的方式同步处理。这被称为事件循环。
队列中的每个消息都有一个与之相关的函数。每次消息离开队列时,在处理任何其他消息之前,该函数将完全执行。这被称为运行到完成。
queue.processNextMessage()
以同步方式等待新消息。正在处理的每个消息都有自己的堆栈,并在堆栈为空之前被处理。一旦所有进程完成,就会从队列中读取新消息,这将继续进行,而有消息存在于队列中。
你可能也听说过 JavaScript 是非阻塞的。当正在处理异步操作时,程序可以继续处理其他事情,例如新输入,而不会阻塞主线程。这个 JavaScript 特性非常有用,事实上,这就是许多人选择在浏览器之外使用该语言的原因。这是一个非常有趣的话题,值得为自己写一篇文章。# 什么是设计模式?
设计模式是解决常见软件开发问题的常用解决方案。
原型模式
如何创建模式?假设你已经识别出一个重复出现的问题,并且你有一个独特的解决方案,该解决方案尚未在任何地方记录,甚至没有在 Stack Overflow 上。你每次遇到此问题时都使用此解决方案,并相信它是可重用的,所有开发人员都将受益于此。
你的解决方案立即成为模式吗?幸运的是,不是。对于具有良好开发实践的人来说,将看起来像模式的东西与实际上是模式的东西混淆是很正常的。
你如何知道你发现的是否真的是设计模式?
通过与其他开发人员共享它,编程是一项由大型社区进行的团队运动。每个模式在成为设计模式之前都必须通过一个阶段,即原型模式。
在成为真正的模式之前,原型模式需要由多个开发人员使用和测试。对于模式被社区认可和使用,需要进行大量的文档工作。
反模式
正如设计模式代表良好实践一样,反模式代表不良实践。
JavaScript 中反模式的一个很好的例子是更改 Object 原型。实际上,JavaScript 中的几乎所有对象都继承自 Object(请记住,JavaScript 使用基于原型的继承),因此想象一下你已更改此原型。更改 Object 的内容将在从它继承的几乎所有对象中都可见-这几乎是 JavaScript 中的所有对象。等待发生的灾难!
另一个例子是更改你不了解的对象。对于在整个应用程序中使用的对象的方法进行小修改可能会造成巨大的混乱,团队越大,混乱就越大。你将遇到命名冲突,不兼容的实现和维护的噩梦。如果运气好,在发生任何事情之前,你的测试会拯救你。
正如了解良好实践一样,了解不良实践也是一个好主意。这是一个识别错误并在为时已晚之前避免它们的好方法。
设计模式类别
设计模式可以按多种方式进行分类,以下是最受欢迎的分类方式:
- 创建型设计模式
- 结构型设计模式
- 行为型设计模式
- 并发设计模式
- 架构设计模式
创建型设计模式
这些模式处理对象的创建,比基本对象创建方式更加优化。当常规对象创建方式会导致更多的复杂性或给代码带来问题时,创建型模式可以解决问题。
结构型设计模式
这些模式处理对象之间的关系。它们确保如果系统的某个部分发生更改,则不需要对其他任何部分进行更改。
行为型设计模式
这种模式认识、实现和改进系统对象之间的通信。它们有助于确保应用程序的不相关部分具有同步信息。
并发设计模式
当你处理多线程编程时,这些模式将非常有用。
架构设计模式
用于系统架构的设计模式,例如 MVC 或 MVVM。
在下一节中,我们将查看一些这些模式的示例,以便更好地理解。
我最喜欢的 9 个设计模式的示例
每个设计模式都代表了解决给定问题的解决方案。没有通用的模式集,可以解决你遇到的每个问题。我们需要了解何时给定模式将有用以及它如何实际提供价值。一旦我们熟悉了设计模式及其适用情况,我们将能够确定可以使用哪种模式以及它是否解决手头的问题。
请记住,使用错误的模式可能会导致不必要的效果、不必要的复杂性和性能损失。
这些都是在我们考虑将设计模式应用于我们的代码时要考虑的重要事项。让我们看一些在 JavaScript 中更有用的模式,每个高级 JavaScript 开发人员都应该了解。
1. 构造函数模式
当我们考虑面向对象语言的经典实现时,构造函数是一种特殊的函数,用于初始化类的默认值或从调用者输入的值。
JavaScript 中创建对象的三种常见方法是:
创建对象后,我们有四种方法将属性添加到它。它们是:
创建对象的最常用方式是使用 {}
,然后使用点符号或 []
添加属性。这就是为什么建议使用这些方法,这将使其他程序员更容易理解你的代码,甚至是你自己。
我们之前提到过 JavaScript 不支持原生类,但通过在函数调用前加上 new
前缀,它支持构造函数。这样,我们可以使用函数作为构造函数,并像更传统的语言一样初始化属性。
我们仍然可以改进此代码。问题在于 writesCode 方法为 Person 的每个实例重新定义。由于 JavaScript 是基于原型的,因此我们可以通过将该方法添加到原型中来避免这种情况。
现在,Person 的两个实例都可以访问相同的 writesCode()
共享实例。
就好像在第一个示例中,每个类型为 Person
的对象都有一个不同的方法,每个 Person
都指向其自己的函数定义一样。在第二个示例中,所有 Person
将指向同一个函数,即相同的代码片段。
2. 模块模式
尽管是面向对象的,但 JavaScript 以其自己独特的方式进行。因为它没有适当的类,所以它也不能限制对类的组件的访问。在像 Java 或 C++ 这样的语言中,你可以定义类成员的访问权限(私有、受保护、公共等),但是作为聪明的小东西,JavaScript 有一种模拟此行为的方法。
在深入了解模块模式的细节之前,让我们更好地了解一下闭包。闭包是具有访问其父范围的函数,即使父项已完成。它们将帮助我们模拟访问限制器的行为。让我们看一个例子:
正如你所看到的,我们将变量 counter 绑定到调用并关闭的函数,但是我们仍然可以通过增加 counter 的子函数访问它。请注意,内部函数从 counterIncrementer()
返回。我们无法从函数外部访问 counter,基本上,我们通过作用域操作在 vanilla JavaScript 中创建了一个私有变量。
使用闭包,我们甚至可以创建具有公共和私有部分的对象。当我们希望隐藏对象的某些部分并仅向模块的用户提供漂亮的界面时,这非常有用。例如:
此模式的最大效用是在对象的公共和私有部分之间进行清晰分离。
并非一切都很愉快,此模式存在一些问题。当我们想要更改成员的可见性时,我们必须在每个调用者上更改它,因为对于公共和私有部分,访问方式不同。另一个问题是在创建对象后添加方法无法访问私有方法(但是我们不想添加新方法)。## 3. 暴露模块模式
这是模块模式的进化版本。主要区别在于我们将所有对象逻辑写在私有作用域中,然后通过匿名对象公开我们想要暴露的内容。我们还可以在将私有成员映射到公共成员时更改私有成员的名称。
暴露模块模式是我们实现模块模式的方式之一。与其他变体的区别主要在于我们引用公共模块的方式。因此,暴露模块模式更容易使用和更改。但在某些情况下,仍然可能会出现问题,例如当我们在继承链上使用对象时。最麻烦的情况是:
- 如果我们有一个引用公共函数的私有函数,我们无法覆盖公共函数。私有函数将继续指向私有实现,从而导致错误。
- 当我们有一个公共成员指向私有成员,并尝试从模块外部覆盖公共成员时,所有其他函数仍将引用私有值,从而引入错误。
4. 单例模式
当我们想要允许类的单个实例时,我们使用单例模式。例如,当我们有一个配置对象时,我们不希望每次调用它时都创建一个新对象,它必须始终是相同的,否则我们可能每次都有不同的设置。
生成的随机数始终相同,就像配置值一样。
5. 观察者模式
当我们想要优化系统不同部分之间的通信时,观察者模式非常有用。它促进了部件的集成,而不会使它们过于耦合。
你将发现有几种不同的实现此模式的方法,但更简单的情况是当我们有一个发射器和许多观察者时。
发射器将执行所有观察者订阅的操作。这些操作可以是订阅主题、取消订阅主题和每次发布主题时通知订阅者。
此模式的一种变体是发布者/订阅者模式,这是我们将在本文中看到的模式。
在观察者模式中,发射器保留所有观察者的引用,并直接在这些对象上调用方法。另一方面,发布者/订阅者模式具有通道,这些通道作为发布者和订阅者之间的通信层。发布者触发事件并只执行发送到此事件的回调函数。
这是发布者/订阅者模式的一个小例子。
当我们想要在不同的位置响应单个事件时,此设计模式特别有用。想象一下,你需要向API发出多个请求,根据响应进行其他调用。你必须嵌套多个回调,这可能很难管理。使用发布者/订阅者模式,你可以以更简单的方式解决此问题。
这种模式的问题是测试。测试发布者和监听器的行为可能会很困难。
6. 中介者模式
在解耦合系统中经常使用的一种模式是中介者。当我们有系统的不同部分需要协调通信时,中介者可能是最佳选择。
与现实生活中一样,中介者是其他对象之间通信的焦点。
乍一看,中介者和发布者/订阅者模式看起来非常相似。实际上,它们都用于管理元素之间的通信。区别在于发布者/订阅者将事件抛向风中并忘记了它,而中介者将确保每个订阅者都处理消息。
中介者模式的一个很好的用例是向导。假设你的系统上有一个长时间的注册流程。通常,大型表单会分成多个步骤。
这是一种使代码维护更容易的方法,同时用户不会被庞大的表单压倒。中介者可以管理整个向导,显示与每个步骤的输入配合使用的不同步骤的用法。
这种模式的明显好处是改善了系统部分之间的通信。与辩论中发生的情况类似,中介者确保每个人都有说话的时间,没有人会乱说话。
如果中介者停止,一切都会停止,这就是这种模式的主要问题。
7. 原型模式
你一定已经厌倦了读这篇文章,但是JavaScript在其本地形式中不支持类。继承是通过原型实现的。
我们可以创建将用作其他对象的原型的对象。原型是构造函数制作的对象的蓝图。几乎就像我们在Java中区分类和对象一样。
让我们看看这个模式的示例。
请注意,通过原型继承最终会带来性能提升,因为两个对象都有对在原型上实现的同一方法的引用,而不是在它们各自上实现该方法。
8. 命令模式
当我们想要将调用封装为对象时,使用命令模式。这是一种将调用者的上下文与所调用的上下文分开的方法。
假设你的JavaScript应用程序有多个对API的调用。现在想象一下,API发生了变化。我们不想更改与API交互的每个位置。
这就是一个抽象层的作用,用于将调用API的对象与确定何时调用它的对象分开。这样,我们就不需要在应用程序上进行大量更改,因为我们只需在一个位置更改对API的调用即可。
这种模式带来的问题是它会创建一个额外的抽象层,可能会影响应用程序的性能。重要的是要知道如何平衡性能和代码可读性。
9. 外观模式
当我们希望在公开显示的内容和内部实现之间创建抽象层时,使用外观设计模式。当我们希望拥有更简单的界面时使用它。
例如,JQuery、Dojo和D3等库的DOM选择器中使用此模式。这些框架具有强大的选择器,使我们可以以非常简单的方式编写复杂的查询。类似于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
评论(0)