ECMAScript-秘籍-全-

ECMAScript 秘籍(全)

原文:zh.annas-archive.org/md5/dcb353bf82ec33c011f5368f5e2a7957

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

JavaScript 像之前的许多语言一样渗透到开发领域。自从 2009 年 5 月 Node.js 运行时的引入以来,它已经远远超出了浏览器的范畴。现在它可以在树莓派上的控制器上工作,作为在桌面计算机上运行的 3D 视频游戏的脚本语言,运行每天服务于数百万页面浏览量的网络服务器,当然,它也是网络浏览器的主导语言。JavaScript 可能是世界上最重要的编程语言。

ECMAScript 标准几乎与 JavaScript 一样历史悠久。然而,在过去的几年里,它经历了大量的活动。2015 年发布的 ES6 创建了一种几乎全新的语言。从那时起,更新更加渐进,但仍然意义重大。本书涵盖了标准直到 ES8(2017 年发布)。我们将讨论如何使用其中的一些新特性来更有效地组织程序并编写更好的代码。

本书面向的对象

本书面向广泛的读者。虽然这不应被视为一本入门书籍,但任何编写过 JavaScript 程序并在浏览器中运行过的人都将为阅读本书做好准备。JavaScript 专家也会发现一些值得思考的内容。

本书涵盖的内容

第一章,使用模块构建,介绍了如何使用 ECMAScript 模块来组织代码以及如何配置现代浏览器来使用它们。本章还涵盖了与谷歌的 Chrome 和 Mozilla 的 Firefox 浏览器的兼容性,以及如何使它们与模块一起工作。

第二章,与旧版浏览器保持兼容性,介绍了如何将使用 ECMAScript 模块的代码转换为不支持它们的平台可以使用的格式。我们使用 webpack,一种常见的 JavaScript 编译工具,将我们的模块组装成一个单一的 JavaScript 文件(称为捆绑包),并在 ECMASript 模块不兼容时将其加载到我们的浏览器中。

第三章,与 Promise 一起工作,介绍了如何使用 Promise API 来组织异步程序。我们将探讨如何通过 Promise 链传播结果和处理错误。Promise 通常被认为比旧的回调模式有所改进。本章还为下一章讨论的主题奠定了基础。

第四章,与 async/await 和函数一起工作,介绍了如何使用新的asyncawait特性。在上一章的基础上,我们将探讨它们如何与 Promise API 替换或协同使用,提高程序的可读性同时保持兼容性。

第五章,Web Workers、共享内存和原子操作,涵盖了可以用来并行处理数据的 Web API。这一章是 JavaScript 作为语言和浏览器作为平台的近期发展的象征。并行编程是 JavaScript 的一个新领域,为语言带来了新的可能性和问题。

第六章,普通对象,展示了使用 API 和语法选项来处理普通对象的方法。我们将探讨如何将对象作为集合来处理,以及如何定义具有一些有趣行为的属性。

第七章,创建类,涵盖了使用 ECMAScript 类语义的使用。我们将创建具有在单个实例和整个类上定义的行为的新类。我们将探讨如何添加属性和定义方法。

第八章,继承和组合,基于前一章的知识;我们将组合类到更大的结构中。我们还将探讨如何使用组合和继承在类之间共享行为,并讨论各自的优缺点。

第九章,使用设计模式构建更大的结构,进一步扩展前两章的内容,探讨了某些特定任务中程序组织的一些常见方式。我们将实现一些常见的设计模式,并演示如何根据不同的用途扩展和修改它们。

第十章,使用数组,涵盖了新Array API 特性的使用。在过去,使用数组意味着大量的循环和跟踪索引,或者导入庞大的库来清理重复的代码。本章将展示一些新的、功能启发的方法,这些方法使得处理这些集合变得更加容易。

第十一章,使用映射和符号,涵盖了如何利用MapWeakMap类在多种类型的值之间建立关系。在本章中,我们将探讨如何使用这两个类的 API,它们之间的区别,以及我们如何控制可以放入它们中的类型。

第十二章,使用集合,展示了SetWeakSet类的使用。当元素的顺序不重要,我们只想知道是否存在某个元素时,这些类非常出色。我们将看到如何使用这两个类的 API,何时使用一个而不是另一个,以及我们如何控制可以放入它们中的类型。

为了充分利用本书

本书假设您具备一些非常基础的知识和资源,以便充分利用它:

  • 一台可以安装程序和配置浏览器的计算机

  • 使用您熟悉的文本编辑器;有很多选项可供选择:

    • VSCode

    • Atom

    • Vim

    • Emacs

  • 一些基本的编程知识。如果您之前没有编写过函数,这可能不是开始的最佳地方。

下载示例代码文件

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/ECMAScript-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有来自我们丰富的图书和视频目录的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“接下来,在同一个目录中,创建一个名为 hello.js 的文件,该文件导出一个名为 sayHi 的函数,该函数向控制台写入消息。”

代码块设置如下:

// hello.js 
export function sayHi () { 
  console.log('Hello, World'); 
} 

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

import rocketName, { launch, COUNT_DOWN_DURATION } from './saturn-v.js'; 
import falconName, { launch as falconLaunch, COUNT_DOWN_DURATION as falconCount } from './falcon-heavy.js'; 

任何命令行输入或输出都按以下方式编写:

cd ~/Desktop/es8-cookbook-workspace

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“双击 nvm-setup。”

警告或重要注意事项看起来像这样。

技巧和窍门看起来像这样。

部分

在本书中,您会发现一些频繁出现的标题(准备工作如何操作如何工作还有更多...也见)。

为了清楚地说明如何完成食谱,请按以下方式使用这些部分:

准备工作

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作...

本节包含遵循食谱所需的步骤。

如何工作...

本节通常包含对上一节发生情况的详细解释。

还有更多...

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

参见

本节提供了有关食谱的其他有用信息的链接。

联系我们

欢迎读者反馈。

一般反馈: 请发送电子邮件至 feedback@packtpub.com 并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请发送电子邮件至 questions@packtpub.com

勘误: 尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上发现我们作品的任何非法副本,我们非常感谢您能提供位置地址或网站名称。请通过链接联系我们在 copyright@packtpub.com

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解 Packt 的更多信息,请访问 packtpub.com

第一章:使用模块构建

在本章中,我们将介绍以下内容:

  • 安装和配置浏览器—Chrome 和 Firefox

  • 安装 Python,使用 SimpleHTTPServer 来托管本地静态文件服务器

  • 创建一个加载 ECMAScript 模块的 HTML 页面

  • 导出/导入多个模块以供外部使用

  • 重命名导入的模块

  • 在单个命名空间下嵌套模块

简介

JavaScript 是最著名的遵循 ECMAScript 标准的语言。该标准是在 20 世纪 90 年代末期创建的,目的是为了指导语言的发展。在早期,发展缓慢,在前二十年里只有四个主要版本达到了生产阶段。然而,随着曝光度的增加,这很大程度上得益于 Node.js 运行时的普及,发展速度显著加快。2015 年、2016 年和 2017 年每年都发布了新的标准版本,2018 年也计划发布另一个版本。

在所有这些发展之后,现在是 JavaScript 开发者激动人心的时刻。许多新的想法来自其他语言,标准 API 正在扩展以提供更多帮助。本书专注于可以在 JS 的新版本以及未来版本中使用的新特性和技术。

从历史上看,创建跨越多个文件的 JavaScript 程序一直是一种痛苦的经历。最简单的方法是将每个文件包含在单独的 <script> 标签中。这也要求开发者正确地定位这些标签。

不同的库试图改善这种情况。RequireJS、Browserify 和 Webpack 都试图解决 JavaScript 依赖项和模块加载的问题。这些都需要某种形式的配置或构建步骤。

近年来,情况有所改善。浏览器制造商合作创建 ECMAScript 规范。然后,制造商需要实现符合该规范的 JavaScript 解释器(实际运行 JavaScript 的程序)。

正在发布支持原生 ECMAScript 模块的浏览器新版本。ECMAScript 模块提供了一种优雅的包含依赖项的方法。最好的是,与之前的方法不同,模块不需要任何构建步骤或配置。

本章中的食谱主要关注安装和配置 Chrome 和 Firefox 网络浏览器,以及如何充分利用 ES 模块和导入/导出语法。

安装和配置 - Chrome

后续的食谱将假设一个能够使用 ES 模块的环境。有两种策略可以实现这一点:创建一个构建步骤,将所有使用的模块收集到一个文件中以便浏览器下载,或者使用能够使用 ES 模块的浏览器。这个食谱演示了后者选项。

准备工作

要逐步完成此菜谱,您需要一个由 Chrome 支持的操作系统(OS)(不是 Chromium)。它支持 Windows 和 macOS 的最新版本,以及大量 Linux 发行版。如果您的操作系统不支持此浏览器,您可能已经知道了这一点。

如何操作...

  1. 要下载 Chrome,将您的浏览器导航到以下位置:

    www.google.co.in/chrome/

  2. 点击下载并接受服务条款。

  3. 安装程序下载完成后,双击安装程序以启动它并按照屏幕上的说明操作。

  4. 检查 Chrome 版本,打开 Chrome 浏览器,并输入以下 URL:

    chrome://settings/help

  5. 您应该看到版本号,数字为 61 或更高。请参阅以下截图:

工作原理...

Chrome 的当前版本默认启用 ES 模块。因此,无需配置或插件即可使其工作!

更多内容...

在撰写本文时,只有少数浏览器支持 ECMAScript。您可以在mzl.la/1PY7nnm页面上的浏览器兼容性部分查看哪些浏览器支持模块。

安装和配置 - Firefox

后续菜谱将假设一个能够使用 ES 模块的环境。有两种策略可以实现这一点:创建一个构建步骤,将所有使用的模块收集到一个文件中以便浏览器下载,或者使用能够使用 ES 模块的浏览器。本菜谱演示了后者选项。

准备工作

要逐步完成此菜谱,您需要一个由 Firefox 支持的操作系统(OS)。它支持 Windows 和 macOS 的最新版本,以及大量 Linux 发行版。如果您的操作系统不支持 Firefox,您可能已经知道了这一点。

如何操作...

  1. 要安装 Firefox,打开浏览器并输入以下 URL:

    https://www.mozilla.org/firefox

  2. 点击显示“下载”的按钮来下载安装程序。

  3. 安装程序下载完成后,双击安装程序并按照屏幕上的说明操作。

  4. 要配置 Firefox,打开 Firefox 浏览器并输入以下 URL:

    about:config

  5. 菜单将允许您启用高级和实验性功能。如果您看到警告,请点击显示“我接受风险!”的按钮!

  6. 找到 dom.moduleScripts.enabled 设置,并双击它将值设置为 true**,如图所示:

工作原理...

Firefox 支持 ES 模块,但默认情况下是禁用的。这允许开发者尝试该功能,而大多数用户不会接触到它。

更多内容...

与“安装和配置 - Chrome”部分相同。

安装 Python,使用 SimpleHTTPServer 来托管本地静态文件服务器

可以直接从文件系统中浏览网页。然而,Chrome 和 Firefox 的安全特性使得这在进行开发时变得不方便。我们需要的是一个简单的静态文件服务器。本食谱演示了如何(如果需要)安装 Python 并使用它从目录中提供文件。

准备工作

查找如何在您的操作系统上打开命令行。在 macOS 和 Linux 上,这被称为终端。在 Windows 上,它被称为命令提示符。

您应该使用配置为加载 ES 模块的浏览器(参见第一个食谱)。

如何操作...

  1. 检查您是否已安装 Python。

  2. 打开命令行。

  3. 输入以下命令:

python --version
  1. 如果您看到如下显示的输出,则表示 Python 已经安装。您可以跳到第 6 步:
Python 2.7.10
  1. 如果您收到如下错误,请继续进行第 5 步的安装:
command not found: python
  1. 在您的计算机上安装 Python:

    • 对于 macOS,从以下链接下载并运行 Python 2 或 3 的最新版本安装程序:www.python.org/downloads/mac-osx/

    • 对于 Windows,从以下链接下载并运行 Python 2 或 3 的最新版本安装程序:www.python.org/downloads/windows/

    • 对于 Linux 系统,请使用操作系统的内置包管理器来安装 Python 包。

  2. 在桌面创建一个名为es8-cookbook-workspace的文件夹。

  3. 在文件夹内创建一个名为hello.txt的文本文件,并将一些文本保存到其中。

  4. 打开命令提示符并导航到该文件夹:

  5. 在 Linux 或 macOS 终端中输入:

    cd ~/Desktop/es8-cookbook-workspace
  1. 在 Windows 上输入以下命令:
    cd C:Desktopes8-cookbook-workspace 
  1. 使用以下命令启动 Python HTTP 服务器:
    python -m SimpleHTTPServer # python 2 

或者我们可以使用以下命令:

python -m http.server # python 3
  1. 打开您的浏览器并输入以下 URL:

    http://localhost:8000/.

  2. 您应该看到一个页面,显示es8-cookbook-workspace文件夹的内容:

  1. 点击 hello.txt 链接,您将看到您创建的文件的文本内容。

它是如何工作的...

我们首先检查 Python 是否已安装。最好的方法是询问 Python 其版本号。这样我们就可以知道 Python 是否已安装,以及它是否足够新以满足我们的需求。

如果尚未安装,可以通过操作系统的包管理器或通过 Python 网站提供的安装程序来获取 Python。

安装完成后,Python 附带了许多实用工具。我们感兴趣的是名为SimpleHTTPServer的实用工具。此实用工具监听端口8000上的 HTTP 请求,并返回相对于目录根的文件内容。如果路径指向目录,则返回一个列出目录内容的 HTML 页面。

创建一个加载 ECMAScript 模块的 HTML 页面

在之前的食谱中,我们介绍了使用 Python 运行静态文件服务器以及配置浏览器使用 ES 模块的安装和配置说明。

准备工作

此菜谱假设你在工作目录中运行了静态文件服务器。如果你还没有安装 Python 或配置浏览器以使用 ES 模块,请参阅本书中的前两个菜谱。

以下步骤将演示如何创建一个 ES 模块并将其加载到 HTML 文件中。

如何操作...

  1. 创建一个包含一些文本内容的 hello.html 文件:
<html>
 <meta charset="UTF-8" />
  <head>
  </head>
  <body>
  Open Your Console!
  </body>
</html>
  1. 通过打开浏览器并输入以下 URL 来打开 hello.htmlhttp://localhost:8000/hello.html

  2. 浏览器应该会显示 Open Your Console!:

图片

  1. 让我们按照页面上的指示打开开发者控制台。对于 Firefox 和 Chrome,命令是相同的:

    • 在 Windows 和 Linux 上:
Ctrl + Shift + I 
  • 在 macOS 上:
Cmd + Shift + I  
  1. 接下来,在同一个目录中创建一个名为 hello.js 的文件,导出一个名为 sayHi 的函数,该函数将消息写入控制台:
// hello.js 
export function sayHi () { 
  console.log('Hello, World'); 
} 
  1. 接下来,将一个脚本模块标签添加到 hello.html 的头部,导入 hello.js 中的 sayHi 方法(注意类型值)。

  2. 打开开发者控制台并重新加载浏览器窗口,你应该会看到文本形式的 hello 消息显示:

图片

它是如何工作的...

虽然我们的浏览器可以处理 ES 模块,但我们仍然需要指定我们希望代码以这种方式加载。较老的方式包括脚本文件使用 type="text/javascript"。这告诉浏览器立即执行标签的内容(无论是从标签内容还是从 src 属性)。

通过指定 type="module",我们告诉浏览器这个标签是一个 ES 模块。此标签内的代码可以导入其他模块的成员。我们从 hello 模块中导入了 sayHi 函数,并在该 <script> 标签内执行了它。我们将在接下来的几个菜谱中深入研究 importexport 语法。

参见

  • 导出/导入多个模块以供外部使用

  • 添加回退脚本标签

导出/导入多个模块以供外部使用

在之前的菜谱中,我们将一个 ES 模块加载到 HTML 页面并执行了一个导出的函数。现在我们可以看看如何在程序中使用多个模块。这使我们组织代码时更加灵活。

准备工作

确保你已经安装了 Python 并正确配置了你的浏览器。

如何操作...

  1. 创建一个新的工作目录,使用你的命令行应用程序导航到该目录,并启动 Python 的 SimpleHTTPServer

  2. 创建一个名为 rocket.js 的文件,导出一个火箭的名称、倒计时持续时间和发射函数:

export default name = "Saturn V"; 
export const COUNT_DOWN_DURATION = 10; 

export function launch () { 
  console.log(`Launching in ${COUNT_DOWN_DURATION}`); 
  launchSequence(); 
} 

function launchSequence () { 
  let currCount = COUNT_DOWN_DURATION; 

  const countDownInterval = setInterval(function () { 
    currCount--; 

    if (0 < currCount) { 
      console.log(currCount); 
    } else { 
      console.log('LIFTOFF!!! '); 
      clearInterval(countDownInterval); 
    } 
  }, 1000); 
}
  1. 创建一个名为 main.js 的文件,从 rocket.js 中导入,输出详细信息,然后调用发射函数:
import rocketName, {COUNT_DOWN_DURATION, launch } from './rocket.js'; 

export function main () { 
  console.log('This is a "%s" rocket', rocketName); 
  console.log('It will launch in  "%d" seconds.', COUNT_DOWN_DURATION); 
  launch(); 
} 
  1. 接下来,创建一个 index.html 文件,导入 main.js 模块并运行 main 函数:
<html> 
  <head> 
    <meta charset='UTF-8' /> 
  </head> 
  <body> 
    <h1>Open your console.</h1> 
    <script type="module"> 
      import { main } from './main.js'; 
      main(); 
    </script> 
  </body> 
</html> 
  1. 打开你的浏览器,然后打开 index.html 文件。你应该会看到以下输出:

图片

它是如何工作的...

从模块导出一个成员有两种选择。它可以作为default成员导出,或者作为命名成员导出。在rocket.js中,我们看到了这两种方法:

export default name = "Saturn V"; 
export const COUNT_DOWN_DURATION = 10; 
export function launch () { ... } 

在这种情况下,字符串"Saturn V"作为默认成员导出,而COUNT_DOWN_DURATIONlaunch作为命名成员导出。我们可以在main.js导入模块时看到这种影响:

 import rocketName, { launch, COUNT_DOWN_DURATION } from './rocket.js'; 

我们可以看到默认成员和命名成员导入的差异。命名成员出现在大括号内,它们导入的名称与模块源文件中的名称匹配。另一方面,默认模块出现在大括号外,可以分配给任何名称。未导出的成员launchSequence不能被另一个模块导入。

参见

  • 重命名导入的模块

  • 将导入的模块嵌套在单个命名空间下

重命名导入的模块

模块允许在组织代码方面有更大的灵活性。这允许使用更短、更具上下文的名字。例如,在前一个菜谱中,我们命名了一个函数为launch,而不是更冗长的名称,如launchRocket。这有助于使我们的代码更易于阅读,但也意味着不同的模块可以导出使用相同名称的成员。

在这个菜谱中,我们将重命名导入以避免这些命名空间冲突。

准备工作

我们将重用前一个菜谱中的代码(导出/导入多个模块以供外部使用)。前一个文件中的更改将被突出显示。

如何做到这一点...

  1. 将为前一个菜谱创建的文件夹复制到一个新目录中。

  2. 使用您的命令行应用程序导航到该目录并启动 Python 服务器。

  3. rocket.js重命名为saturn-v.js,在日志语句中添加火箭的名称,并更新main.js的导入语句:

// main.js 
import name, { launch, COUNT_DOWN_DURATION } from './saturn-v.js'; 

export function main () { 
  console.log('This is a "%s" rocket', name); 
  console.log('It will launch in  "%d" seconds.', COUNT_DOWN_DURATION); 
  launch(); 
} 
// saturn-v.js 
export function launch () { 
 console.log(`Launching %s in ${COUNT_DOWN_DURATION}`, name); 
  launchSequence(); 
} 

function launchSequence () { 
  // . . .  
 console.log(%shas LIFTOFF!!!', name); // . . . }
  1. saturn-v.js复制到名为falcon-heavy.js的新文件中,并更改默认导出值和COUNT_DOWN_DURATION
    export default name = "Falcon Heavy";
    export const COUNT_DOWN_DURATION = 5;  
  1. falcon模块导入main.js。重命名导入的成员以避免冲突,并启动 falcon 火箭:
import rocketName, { launch, COUNT_DOWN_DURATION } from './saturn-v.js'; 
import falconName, { launch as falconLaunch, COUNT_DOWN_DURATION as falconCount } from './falcon-heavy.js'; 

export function main () { 
  console.log('This is a "%s" rocket', rocketName); 
  console.log('It will launch in  "%d" seconds.', COUNT_DOWN_DURATION); 
  launch(); 

 console.log('This is a "%s" rocket', falconName); console.log('It will launch in  "%d" seconds.', falconCount); falconLaunch(); 
} 
  1. 在您的浏览器中打开index.html,您应该看到以下输出:

它是如何工作的...

当我们将saturn-v.js文件复制到并从falcon-heavy.js导入成员时,我们遇到了潜在的命名空间冲突。这两个文件都导出了名为COUNT_DOWN_DURATIONlaunch的成员。但是使用as关键字,我们重命名了这些成员以避免冲突。现在导入的main.js文件可以无问题地使用这两组成员。

重命名成员也可以有助于添加上下文。例如,即使没有冲突,将启动重命名为launchRocket也可能很有用。这为导入的模块提供了额外的上下文,并使代码更加清晰。

在单个命名空间下嵌套模块

随着模块数量的增长,模式开始出现。出于实际和架构的原因,将多个模块组合在一起并作为一个单一包使用是有意义的。

这个配方演示了如何将多个模块收集在一起,并将它们作为一个单一包使用。

准备工作

将从之前的配方中提供源代码,以便启动这个配方。否则,您需要参考导出/导入多个模块以供外部使用以了解如何创建index.html文件。

如何做...

  1. 创建一个包含index.html文件的新文件夹,如导出/导入多个模块以供外部使用中所示。

  2. 在该目录内,创建一个名为rockets的文件夹。

  3. rockets内部创建三个文件:falcon-heavy.jssaturn-v.jslaunch-sequence.js

// falcon-heavy.js 
import { launchSequence } from './launch-sequence.js'; 

export const name = "Falcon Heavy"; 
export const COUNT_DOWN_DURATION = 5; 

export function launch () { 
  launchSequence(COUNT_DOWN_DURATION, name); 
} (COUNT_DOWN_DURATION); 
} 

// saturn-v.js 
import { launchSequence } from './launch-sequence.js'; 

export const name = "Saturn V"; 
export const COUNT_DOWN_DURATION = 10; 

export function launch () { 
  launchSequence(COUNT_DOWN_DURATION, name); 
} 

// launch-sequence.js 
export function launchSequence (countDownDuration, name) { 
  let currCount = countDownDuration; 
  console.log(`Launching in ${COUNT_DOWN_DURATION}`, name); 

  const countDownInterval = setInterval(function () { 
    currCount--; 

    if (0 < currCount) { 
      console.log(currCount); 
    } else { 
      console.log('%s LIFTOFF!!! ', name); 
      clearInterval(countDownInterval); 
    } 
  }, 1000); 
} 
  1. 现在创建index.js,该文件导出这些文件的成员:
import * as falconHeavy from './falcon-heavy.js'; 
import * as saturnV from './saturn-v.js'; 
export { falconHeavy, saturnV }; 
  1. 创建一个main.js文件(在包含rockets的文件夹中),该文件从index.js文件中导入falconHeaveysaturnV并启动它们:
import { falconHeavy, saturnV } from './rockets/index.js' 

export function main () { 
  saturnV.launch(); 
  falconHeavy.launch(); 
} 
  1. 在浏览器中打开,可以看到以下输出:

如何工作...

index.js的前两行中看到的语法导入了同一对象下的所有导出成员。这意味着falcon-heavey.jsnameCOUNT_DOWN_DURATIONlaunch成员都附加到了falconHeavy变量上。同样,对于saturn-v.js模块和saturnV变量也是如此。因此,当在第 4 行导出falconHeavysaturnV时,这些导出的名称现在包含了它们各自模块的所有导出成员。

这提供了一个单一的位置,另一个模块(在这种情况下是main.js)可以导入这些成员。这种模式有三个优点。它很简单;只有一个文件可以导入成员,而不是多个。它是统一的,因为所有包都可以使用一个index模块来暴露多个模块的成员。它更加灵活;某些模块的成员可以在整个包中使用,而不需要由index模块导出。

还有更多...

可以直接导出命名项。考虑以下文件,atlas.js

import { launchSequence } from './launch-sequence.js'; 

const name = 'Atlas'; 
const COUNT_DOWN_DURATION = 20; 

export const atlas = { 
  name: name, 
  COUNT_DOWN_DURATION: COUNT_DOWN_DURATION, 
  launch: function () { 
    launchSequence(COUNT_DOWN_DURATION, name); 
  } 
}; 

atlas成员可以直接由index.js导出:

import * as falconHeavy from './falcon-heavy.js'; 
import * as saturnV from './saturn-v.js'; 

export { falconHeavy, saturnV }; 
export { atlas } from './atlas.js';

然后main.js文件可以导入atlas成员并启动它:

import { atlas, falconHeavy, saturnV } from './rockets/index.js' 

export function main () { 
  saturnV.launch(); 
  falconHeavy.launch(); 
 atlas.launch(); 
} 

这始终使用命名导出的一个好处是,更容易从具有多个模块的包中收集和导出特定的成员。

不论是否命名,嵌套是一种用于分组模块的绝佳技术。随着模块数量的持续增长,它提供了一种组织代码的机制。

第二章:与旧版浏览器保持兼容

在本章中,我们将介绍以下配方:

  • 使用 NVM 安装 Node.js

  • 安装和配置 webpack

  • 添加回退脚本标签以加载客户端包

  • 使用 Babel Polyfill 模拟方法

  • 使用 Babel 支持新语言功能

  • 使用工具分析 webpack 包

简介

在上一章中,我们介绍了如何利用新的 ECMAScript 模块从多个文件加载代码并组织我们的代码。这项前沿技术最近才在浏览器中可用。在实践中,生产网站试图针对尽可能多的用户。这通常意味着针对旧浏览器。此外,JavaScript 还运行在其他环境(如 Node.js)中,这些环境不支持 ECMAScript 模块。

好消息是,我们不需要更改源代码来支持这些平台。有工具可以将多个源文件生成一个单一的 JavaScript 文件。这样我们就可以使用模块来组织我们的代码,并在更多平台上运行我们的程序。

本章中的配方侧重于 webpack 的安装和配置,以便为不支持 ES 模块和其他语言更新添加的平台提供回退选项。

使用 NVM 在 Linux 和 macOS 上安装 Node.js

Node 在其网站上提供了 Windows 和 macOS 的安装二进制文件:

nodejs.org/en/download/.

通过下载适用于您的操作系统和处理器的适当安装程序来安装 Node.js 很容易。然而,拥有一个版本管理器很有用,这样您就可以在需要不同版本的项目上工作,并使用最新版本。这对于您的包管理器不提供 Node.js 的最新版本(例如,Ubuntu)尤其有用。

后续配方将假设已安装 Node.js。此配方演示了如何在 Linux 和 macOS 上安装 Node.js。下一配方将涵盖 Windows 的安装说明。

准备工作

此配方仅适用于 Linux 和 macOS。有关 Windows 说明,请参阅下一配方。

您必须已安装 git。它在 macOS 上已安装,Linux 发行版应通过其包管理器提供 git。

如何操作...

  1. 打开您的命令行应用程序。

  2. nvm项目克隆到您的家目录中的一个目录中:

git clone https://github.com/creationix/nvm.git ~/.nvm
  1. 将以下代码添加到您的~/.bashrc~/.zshrc文件的底部。如果您不知道您正在运行什么 shell,它可能是 bash,您应该将条目添加到~/.bashrc
# Configure NVM 
export NVM_DIR="$HOME/.nvm" 
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" 
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"   
  1. 保存文件并返回到您的命令行:
source ~/.bashrc # (or ~/.zshrc if you're running zshell)
  1. 通过检查nvm的版本号来确认您的安装:
> nvm --version 0.33.5
  1. 列出所有可安装的 node 版本:
> nvm list-remote
  1. 安装最新的长期支持LTS)或稳定版本。(在撰写本文时,版本 8.9.4 是最新 LTS 版本):
    > nvm install 8.9.4
  1. 通过检查nodenpm的版本号来确认它们的安装:
    >  node --version
    v8.9.4
    > npm --version
    5.6.0

它是如何工作的...

此安装的关键是 步骤 4步骤 5步骤 4 确保了 nvm 可执行文件是您环境 PATH 的一部分,并且设置了相关环境变量。当您运行 nvm install 6.11 时,nvm 可执行文件将被运行,node 二进制文件将被安装到预期位置。

更多内容...

如果我们还安装了版本 v9.6.1,我们可以使用以下命令将其设置为默认版本:

    nvm alias default 9.6.1 

现在我们打开一个新的 shell,v9.6.1 将是正在使用的 Node.js 版本。

使用 NVM 安装 Node.js:Windows

后续的食谱将假设已经安装了 Node.js。本食谱演示了如何在 Windows 上安装 Node.js。

准备工作

本食谱适用于 Windows 环境。有关 macOS 和 Linux 指令,请参阅前面的食谱。

您还必须安装 git。您可以从以下链接下载 git:

git-scm.com/download/win

如何操作...

  1. 访问项目发布页面:

    github.com/coreybutler/nvm-windows/releases

  2. 下载最新的 nvm-setup.zip 文件。

  3. 解压下载的 ZIP 文件。

  4. 双击 nvm-setup

  5. 通过向导完成安装。

  6. 打开命令提示符。

  7. 通过检查 nvm 的版本号来确认安装:

    > nvm version
    1.1.6 
  1. 列出所有可安装的 Node.js 版本:
    > nvm list available
  1. 安装最新的 LTS 或稳定版本(截至编写时,版本 8.9.4 是最新的 LTS 版本):
    > nvm install 8.9.4
    6.11.0
    Downloading node.js version 8.9.4 (64-bit)...
    Complete
    Creating C:UsersrtharAppDataRoamingnvmtemp

    Downloading npm version 5.6.0... Complete
    Installing npm v5.6.0...

    Installation complete. 
  1. 输入以下命令以激活此版本:
    nvm use 8.9.6

工作原理...

安装程序会下载安装向导。安装向导会下载与 nvm 相关的可执行文件,并调整 PATH 环境变量。PATH 用于在命令行上执行程序时查找程序。如果一个程序位于 PATH 中找到的文件夹之一中,那么它可以不引用其绝对或相对路径而执行。

更多内容...

如果我们还安装了版本 v9.6.1,我们可以使用以下命令将其设置为默认版本:

    nvm use 9.6.1

现在我们打开一个新的 shell,v8.6.0 将是正在使用的 Node.js 版本。与 macOS 和 Linux 版本不同,最后选择的版本将通过 shell 会话保持,并且不需要设置默认版本。

安装和配置 webpack

如前所述,创建 JavaScript 包有几个选项。Rollup 和 Babel 是流行的工具,可以执行此任务。webpack 是一个好的选择,因为它被广泛使用,并且拥有庞大的插件库。

本食谱演示了如何安装和配置 webpack 以构建 JavaScript 包。

准备工作

您需要安装 Node.js。如果没有,请参阅使用 nvm 安装 Node.js 的相应食谱。

如何操作...

  1. 打开您的命令行应用程序,导航到您的 workspace,并创建一个新的 node 包:
    mkdir 02-creating-client-bundles
    cd 02-creating-client-bundles
    npm init -y
  1. 从第一章中的Nesting modules under a single namespace配方复制main.js文件:
// main.js 
import { atlas, saturnV } from './rockets/index.js' 

export function main () { 
  saturnV.launch(); 
  atlas.launch(); 
} 
  1. 创建rockets依赖目录(这些文件可以从第一章中的Nesting modules under a single namespace配方复制而来,使用模块构建):
// rockets/index.js 
import * as saturnV from './saturn-v.js'; 
import * as atlas from './atlas.js'; 
export { saturnV, atlas }; 

// rockets/launch-sequence.js 
export function launchSequence (countDownDuration, name) { 
  let currCount = countDownDuration; 
  console.log(`Launching in ${countDownDuration}`, name); 

  const countDownInterval = setInterval(function () { 
    currCount--; 

    if (0 < currCount) { 
      console.log(currCount); 
    } else { 
      console.log('%s LIFTOFF!!! ', name); 
      clearInterval(countDownInterval); 
    } 
  }, 1000); 
} 

// rockets/atlas.js 
import { launchSequence } from './launch-sequence.js'; 

const name = 'Atlas'; 
const COUNT_DOWN_DURATION = 20; 

export function launch () { 
  launchSequence(COUNT_DOWN_DURATION, name); 
} 

// rockets/saturn-v.js 
import { launchSequence } from './launch-sequence.js'; 

export const name = "Saturn V"; 
export const COUNT_DOWN_DURATION = 10; 

export function launch () { 
  launchSequence(COUNT_DOWN_DURATION, name); 
} 
  1. 创建一个index.js文件,该文件加载并执行main.js中的主函数:
// index.js 
import { main } from './main.js'; 
main();  
  1. 安装 webpack:
   > npm install --save-dev Webpack
  1. 创建一个名为webpack.config.js的 webpack 配置文件,入口点为index.js,输出文件名为bundle.js:
// webpack.config.js 
const path = require('path'); 

module.exports = { 
  entry: './index.js', 
  output: { 
    filename: 'bundle.js', 
    path: path.resolve(__dirname) 
  } 
}; 
  1. package.json中添加一个构建脚本:
{
  /** package.json content**/
  "scripts": {
    "build": "webpack --config webpack.config.js"
  }
}
  1. 运行 webpack 构建以创建bundle.js:
 > npm run build
  1. 你应该会看到描述构建创建和其中包含的模块的输出。请参阅以下输出:
Hash: 5f2f1a7c077186c7a7a7 
Version: webpack 3.6.0 
Time: 134ms 
    Asset    Size  Chunks             Chunk Names 
bundle.js  6.7 kB       0  [emitted]  main 
   [0] ./rockets/launch-sequence.js 399 bytes {0} [built] 
   [1] ./index.js 42 bytes {0} [built] 
   [2] ./main.js 155 bytes {0} [built] 
   [3] ./rockets/index.js 162 bytes {0} [built] 
   [4] ./rockets/falcon-heavy.js 206 bytes {0} [built] 
   [5] ./rockets/saturn-v.js 203 bytes {0} [built] 
   [6] ./rockets/atlas.js 270 bytes {0} [built]  
  1. 使用node运行生成的bundle.js文件:
 node ./bundle.js 
  1. 你应该看到火箭倒计时并发射。

它是如何工作的...

使用npm安装 webpack 会将发布的包下载到node_modules目录。因为 webpack 还包括一个可执行文件,所以它被安装到node_modules/.bin下。

webpack 配置相当简单。它指定了一个入口点和输出点。入口点定义了 webpack 开始遍历的位置。接下来,它访问由入口点导入的所有模块,然后是那些模块导入的所有模块。这个过程会一直重复,直到所有依赖项都被访问。

然后,所有依赖项都被合并到一个文件中。文件位置由输出设置定义。在这种情况下,输出被定义为bundle.js。输出捆绑包被放置在当前目录中。

你可以在webpack.js.org/查看 webpack 文档,了解更多关于其工作原理以及如何针对不同场景进行配置的详细信息。

添加回退脚本标签以加载客户端捆绑包

在之前的配方中,我们展示了如何使用 webpack 组合多个模块。本配方演示了如何将这些模块加载到不支持 ES 模块的浏览器中。

准备工作

本配方假设你已经安装并配置了 webpack。建议你在继续本配方之前完成之前的配方,安装和配置 webpack

你还需要安装 Python。如果你还没有安装,请访问第一章中的安装 Python,使用SimpleHTTPServer托管本地静态文件服务器配方,以及使用模块构建

如何操作...

  1. 打开你的命令行应用程序,导航到包含02-creating-client-bundles包的目录。

  2. 启动 Python HTTP 服务器。

  3. 创建一个名为index.html的文件(从第一章中的Nesting modules under a single namespace配方复制而来,使用模块构建):

<html> 
  <head> 
    <meta charset='UTF-8' /> 
  </head> 
  <body> 
    <h1>Open your console.</h1> 
    <script type="module"> 
      import { main } from './main.js'; 
      main(); 
    </script> 
  </body> 
</html> 
  1. <script>标签中现有的module之后添加一个nomodule脚本标签:
  <body> 
    <h1>Open your console.</h1> 
    <script type="module"> 
      import { main } from './main.js'; 
      main(); 
    </script> 
 <script nomodule type="text/javascript"src="img/bundle.js"></script> 
  </body> 
  1. 运行webpack构建命令:
    ./node_modules/.bin/webpack --config webpack.config.js
  1. 打开您的 ES 模块兼容浏览器,打开开发者工具到网络标签,并访问 URL:

    http://localhost:8000/.

  2. 您应该看到浏览器加载的各个文件:

  1. 打开一个不兼容 ES 模块的浏览器。打开开发者工具中的网络,并访问 URL:

    http://localhost:8000/.

  2. 您应该看到加载了 bundle.js 文件:

它是如何工作的...

在之前的配方中,我们看到了带有属性 type="module" 的脚本标签将被执行并像 ES 模块一样处理。不支持 ES 模块的浏览器根本不会执行此脚本。

如果我们插入一个普通的脚本标签,它也会被较新的浏览器执行。为了避免运行重复的代码,我们使用 nomodule 属性。这告诉支持 ES 模块的新浏览器忽略 ID。

因此,我们得到了期望的行为。模块标签由兼容的浏览器执行,并由旧浏览器忽略。nomodule 属性的脚本由 ES 模块兼容的浏览器忽略并由旧浏览器执行。

在撰写本文时,nomodule 是一个实验性功能,并非所有浏览器都支持。然而,它可能在将来得到支持。

相关内容

  • 使用 Babel 镜像新特性

使用 Babel Polyfill 镜像方法

在前两个配方中,我们看到了如何创建客户端包并将其加载到浏览器中。这使得在源代码中使用 ES 模块成为可能,同时不会破坏与旧浏览器的兼容性。

然而,语言的新版本中也有新的方法可用,我们将在后面的章节中使用。

本配方演示了如何使用 babel-polyfill 库来支持这些方法。

准备工作

本配方假设您已经创建了本章早期配方中的代码,并且已经安装了 Python,并知道如何启动静态 HTTP 服务器。请访问早期配方或复制代码。

如何做到这一点...

  1. 打开您的命令行应用程序,导航到包含 02-creating-client-bundles 包的目录。

  2. 启动 Python HTTP 服务器。

  3. 更新 main.js 文件以使用 Array.prototype.values 方法,并使用 for..of 循环遍历生成的迭代器:

import { atlas, saturnV } from './rockets/index.js' 

export function main () { 
 const rockets = [saturnV, atlas]; for (const rocket of rockets.values()) { rocket.launch(); } 
}  
  1. 安装 Babel Polyfill 包:
    npm install --save babel-polyfill  
  1. 要镜像包,更新 webpack.config.js 文件以将 Babel Polyfill 添加到入口点:
const path = require('path'); 

module.exports = { 
 entry: ['babel-polyfill', './index.js'], 
  output: { 
    filename: 'bundle.js', 
    path: path.resolve(__dirname) 
  } 
};  
  1. 要镜像 ES 模块,您需要直接导入该文件。更新 index.html 以导入 polyfill:
<!-- index.html --> 
<script type="module"> 
 import './node_modules/babel-polyfill/dist/polyfill.min.js'; 
  import { main } from './main.js'; 
  main(); 
</script>  
  1. 现在打开一个浏览器,打开开发者控制台,并访问 URL:

    http://localhost:8000/.

  2. 不论浏览器是否支持 Array.prototype.values,代码都应该运行并显示如下输出:

它是如何工作的...

babel-polyfill 包,恰如其分地,提供了所谓的 polyfill。polyfill 填补了旧浏览器在 ECMAScript 规范中留下的空白。

在上一个例子中,恰好当前版本,61,的 Chrome 没有实现 Array.prototype.values 方法。polyfill 代码在主函数之前运行。它会检查 Array.prototype 对象上是否实现了 values 方法。如果没有原生的实现,那么 polyfill 会实现这个方法。如果已经实现了,那么 polyfill 就会保留原生的实现。

以这种方式,polyfill 库使大量新的方法变得可用。

使用 Babel 支持新语言特性

在上一个配方中,我们看到了如何使用 babel-polyfill 库来支持新的 ES 方法。这个库在运行时向语言添加方法,以便依赖于它们的源代码能够正确运行。

ECMAScript 中有一些相对较新的语言特性,例如箭头函数、letconst 变量声明以及扩展运算符。这些特性并不被普遍支持。Babel 提供了一种机制,可以在源代码级别使用它们,同时保持与构建步骤的兼容性。

这个配方演示了如何在 webpack 中使用 Babel,以便在旧浏览器中支持这些特性。

准备工作

这个配方假设你已经创建了本章早期配方中的代码,并且已经安装了 Python。请访问这些早期配方或复制代码。

如何操作...

  1. 打开你的命令行应用程序并导航到包含 02-creating-client-bundles 包的目录。

  2. 启动 Python HTTP 服务器。

  3. 更新 main.js 文件以使用箭头函数语法:

import { atlas, saturnV } from './rockets/index.js' 

export function main () { 
  const rockets = [saturnV, atlas]; 
 rockets.map((rocket) => rocket.launch() ); 
} 
  1. 安装 Babel、preset-es2015 以及相关的 webpack 加载器:
    npm install --save-dev babel-cli babel-preset-es2015 babel-loader
  1. 创建一个名为 .babelrc 的 Babel 配置文件:
// .babelrc 
{ 
  "presets": ["es2015"] 
} 
  1. 配置 webpack 以使用 Babel 进行新语言特性的转换:
const path = require('path'); 

module.exports = { 
  entry: ['babel-polyfill', './index.js'], 
  output: { 
    filename: 'bundle.js', 
    path: path.resolve(__dirname) 
 }, module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: 'babel-loader' } ] } 
};  
  1. 将 webpack 构建命令添加到 package.json 文件的脚本部分:
{
  /* package.json configuration */

  "scripts": {
    "bundle": "webpack --config webpack.config.js",
  }

  /* remaining properties */
}   
  1. 运行 webpack 构建:
npm run bundle  
  1. 现在,打开浏览器并访问以下 URL 时打开开发者控制台:

    http://localhost:8000/

  2. 你应该看到代码运行正确:

它是如何工作的...

Babel 项目提供了一个编译器,通常称为 转译器。转译器是一个程序,它接受源代码并生成一些目标代码。Babel 转译器的最常见用途是将 JavaScript 源文件转换为更新的特性。

当转译器遇到需要翻译的语言特性的表达式时,它会生成一个逻辑上等效的表达式。生成的表达式可以与源表达式非常相似,也可以非常不同。

想要了解更多关于如何使用 Babel 支持不同平台的信息,请访问其网站:

babeljs.io/

还有更多...

上一个配方使用了 ES2015 预设。这意味着无论当前浏览器的支持情况如何,Babel 总是会生成 ES2015(ES5)兼容的代码。

Babel 的env预设更为复杂,它使用平台兼容性来确定哪些语言特性需要被翻译。请参见项目 readme 中的以下示例:

 // .baberc 
{ 
  "presets": [ 
    ["env", { 
   "targets": { 
     "browsers": ["last 2 versions", "safari >= 7"] 
   } 
 }] 
  ] 
} 

之前的配置针对的是除 Safari 以外的所有浏览器的最后两个版本,而 Safari 则从版本 7 开始一直被针对。这个项目允许 Babel 在浏览器实现更多语言特性时丢弃不再需要的翻译。

你可以在babel-preset-env项目的仓库中找到更多文档和支持:github.com/babel/babel-preset-env

使用工具分析 webpack 包

将代码转换并使用 Polyfills 的一个主要缺点是源代码可能会与源代码有相当大的差异。这通常会导致包的大小膨胀。如果你查看添加 Polyfill 库后的bundle.js文件的大小(参见前两个菜谱),那么你会发现它超过了 200Kb。与没有 Polyfill 的 5Kb 相比,这相当大。

对于许多包,很难找出哪些文件导致了文件大小的增加,以及它们之间的依赖关系。

在这个菜谱中,我们将看到如何使用分析工具来更好地了解我们的 webpack 包。

准备工作

如果从之前的菜谱中获取源代码来启动这个菜谱会有所帮助。否则,你需要参考来自第一章的导出/导入多个模块以供外部使用菜谱,使用模块构建,了解如何创建index.html文件。

如何做...

  1. 打开你的命令行应用程序并导航到包含02-creating-client-bundles包的目录。

  2. 运行 webpack 并将配置输出到 JSON 文件:

./node_modules/.bin/webpack --config webpack.config.js  --profile --json > compilation-stats.json
  1. 要查看占用你包最多空间的模块,打开你的浏览器并访问以下 URL:

    chrisbateman.github.io/webpack-visualizer/.

  2. 拖放文件或使用文件选择器选择编译统计文件compilation-stats.json

  3. 你应该会看到一个图表,它将提供可悬停的模块大小信息:

图片

  1. 现在你已经知道了哪些模块很大,你可以查找依赖关系。访问 webpack 分析器主页:webpack.github.io/analyse/

  2. 拖放文件或使用文件选择器选择编译统计文件compilation-stats.json

  3. 文件加载后,你应该会看到界面发生变化。点击页眉中的“模块”行。

  4. 从这里,你可以看到单个模块以及依赖关系在哪里:

图片

如何工作...

步骤 2中,我们运行了 webpack 命令,并将统计信息输出到一个 JSON 文件中。这些数据包括诸如文件大小贡献和依赖项等信息。我们打开的网站读取了这些 JSON 数据,并生成了可视化。第一个网站,WEBPACK VISUALIZER (chrisbateman.github.io/webpack-visualizer/),使用文件大小数据并强调每个包对整体包大小的贡献。这对于识别导致包大小膨胀的罪魁祸首非常有用。

不幸的是,移除大型依赖项并不总是容易的。依赖项可能有多层深度且难以查找。一旦我们知道是什么导致了大型包的大小,那么我们就可以使用第二个工具,Webpack Visualizer,来提取它们。

第三章:与承诺一起工作

在本章中,我们将介绍以下配方:

  • 创建并等待承诺

  • 解决承诺结果

  • 拒绝承诺错误

  • 链式调用承诺

  • 使用 Promise.resolve 开始承诺链

  • 使用 Promise.all 解析多个承诺

  • 使用 Promise.catch 处理错误

  • 使用 Promise API 模拟 finally

简介

在 JavaScript 的早期版本中,回调模式是最常见的组织异步代码的方式。它完成了工作,但扩展性不好。随着更多异步函数的添加,代码变得更加嵌套,添加、重构和理解代码变得更加困难。这种情况通常被称为 回调地狱

承诺被引入以改善这种情况。承诺允许异步操作之间的关系以更多自由和灵活性进行重新排列和组织。

本章中的配方展示了如何使用承诺创建和组织异步函数,以及如何处理错误情况。

创建并等待承诺

承诺提供了一种以有组织和易于阅读的方式组合和组合异步函数的方法。此配方演示了承诺的非常基本的使用。

准备工作

此配方假设您已经有一个允许您在浏览器中创建和运行 ES 模块的开发空间。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的开发空间。

  2. 创建一个名为 03-01-creating-and-waiting-for-promises 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件创建一个承诺并在创建承诺前后以及承诺执行期间和解决后记录消息:

// main.js 
export function main () { 

  console.log('Before promise created'); 

  new Promise(function (resolve) { 
    console.log('Executing promise'); 
    resolve(); 
  }).then(function () { 
    console.log('Finished promise'); 
  }); 

  console.log('After promise created'); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您将看到以下输出:

它是如何工作的...

通过查看日志消息的顺序,您可以清楚地看到操作顺序。首先,执行初始日志。接下来,使用执行器方法创建承诺。执行器方法将 resolve 作为参数。resolve 函数实现承诺。

承诺遵循名为 thenable 的接口。这意味着我们可以链式调用 then 回调。我们使用此方法附加的回调在调用 resolve 函数之后执行。此函数异步执行(不是在承诺解决后立即执行)。

最后,在创建承诺后有一个日志记录。

日志消息出现的顺序揭示了代码的异步性质。所有日志都按其在代码中出现的顺序显示,除了“承诺完成”消息。该函数在 main 函数退出后异步执行!

在本章后面的示例中,我们将更详细地探讨 resolvethen 以及承诺 API 的其他部分。

解决承诺结果

在上一个示例中,我们看到了如何使用承诺来执行异步代码。然而,这段代码相当基础。它只是记录一条消息然后调用 resolve。通常,我们希望使用异步代码执行一些长时间运行的操作,然后返回该值。

本示例演示了如何使用 resolve 来返回长时间运行操作的结果。

准备工作

本示例假设您已经有一个允许您在浏览器中创建和运行 ES 模块的开发空间。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的开发空间。

  2. 创建一个名为 3-02-resolving-promise-results 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件创建一个承诺并在创建承诺前后记录消息:

// main.js 
export function main () { 

  console.log('Before promise created'); 

  new Promise(function (resolve) { 
  }); 

  console.log('After promise created'); 
} 
  1. 在承诺内部,在 5 秒超时后解决一个随机数:
    new Promise(function (resolve) { 
setTimeout(function () {
 resolve(Math.random()); }, 5000); 
    }) 
  1. 在承诺上链式调用一个 then 调用。传递一个函数,该函数输出其唯一参数的值:
   new Promise(function (resolve) { 
      setTimeout(function () { 
        resolve(Math.random()); 
      }, 5000); 
 }).then(function (result) { console.log('Long running job returned: %s', result); });
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出:

工作原理...

正如上一个示例中所示,承诺直到 resolve 执行(这次是在 5 秒后)才得到解决。然而,这次我们立即传递一个随机数作为 resolve 的参数。当这种情况发生时,该参数被提供给后续 then 函数的回调。我们将在未来的示例中看到如何继续创建 承诺链

拒绝承诺错误

在上一个示例中,我们看到了如何使用 resolve 从成功解决的承诺中提供结果。不幸的是,代码并不总是按预期运行。网络连接可能会断开,数据可能会损坏,以及无法计数的其他错误可能会发生。我们需要能够处理这些情况。

本示例演示了当出现错误时如何使用 reject

准备工作

本示例假设您已经有一个允许您在浏览器中创建和运行 ES 模块的开发空间。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的开发空间。

  2. 创建一个名为 3-03-rejecting-promise-errors 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件创建一个承诺,并在创建承诺前后以及承诺解决时记录消息:

   new Promise(function (resolve) { 
     resolve(); 
      }).then(function (result) { 
     console.log('Promise Completed'); 
   }); 
  1. 向承诺回调添加一个名为 reject 的第二个参数,并使用新的错误调用 reject
    new Promise(function (resolve, reject) { 
 reject(new Error('Something went wrong'); 
    }).then(function (result) { 
    console.log('Promise Completed'); 
   }); 
  1. 在承诺上链式调用一个 catch 调用。传递一个函数,该函数输出其唯一的参数:
    new Promise(function (resolve, reject) { 
 reject(new Error('Something went wrong'); 
     }).then(function (result) { 
     console.log('Promise Completed'); 
 }).catch(function (error) { console.error(error); });
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

在之前的例子中,我们看到了如何在承诺成功履行的情况下使用 resolve 返回一个值。在这种情况下,我们在 resolve 之前调用了 reject。这意味着承诺在能够 resolve 之前就已经因为错误而结束了。

当承诺在错误状态下完成时,then 回调函数不会执行。相反,我们必须使用 catch 来接收承诺拒绝的错误。你也会注意到,catch 回调函数只有在 main 函数返回之后才会执行。像成功的履行一样,对不成功的履行者的监听器也是异步执行的。

参见

  • 使用 Promise.catch 处理错误

  • 使用 Promise.then 模拟 finally

链式调用承诺

到目前为止,在本章中,我们已经看到了如何使用承诺来运行单个异步任务。这很有帮助,但与回调模式相比并没有带来显著改进。承诺真正的优势在于它们可以组合使用。

在这个菜谱中,我们将使用承诺来串联异步函数。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 3-04-chaining-promises. 的新文件夹

  3. 复制或创建一个 index.html,它从 main.js 加载并运行一个 main 函数。

  4. 创建一个 main.js 文件,该文件创建一个承诺。从承诺中解析一个随机数:

   new Promise(function (resolve) { 
     resolve(Math.random()); 
   }); 
);  
  1. 从承诺中链式调用一个 then 调用。如果随机值大于或等于 0.5,则从回调函数返回 true
    new Promise(function (resolve, reject) { 
         resolve(Math.random()); 
 }).then(function(value) {
 return value >= 0.5; });
  1. 在前一个 then 调用之后链式调用一个最终的 then 调用。如果参数是 truefalse,则输出不同的消息:
  new Promise(function (resolve, reject) { 
    resolve(Math.random()); 
  }).then(function (value) { 
    return value >= 0.5; 
 }).then(function (isReadyForLaunch) { if (isReadyForLaunch) { console.log('Start the countdown! ![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/es-cb/img/f7032fb1-8d2d-4e6e-a0ab-03ac5a186a06.png)'); } else { console.log('Abort the mission. ![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/es-cb/img/4fac89e8-1ce7-4abd-936a-8346d71ba864.png)'); } });
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 如果你很幸运,你会看到以下输出:

  1. 如果你运气不好,我们会看到以下输出:

它是如何工作的...

我们已经看到了如何使用 then 等待承诺的结果。在这里,我们连续多次做同样的事情。这被称为承诺链。在启动新的承诺后,承诺链中的所有后续链接也返回承诺。也就是说,每个 then 函数的回调就像另一个承诺一样是 resolve

参见

  • 使用 Promise.all 解析多个承诺

  • 使用 Promise.catch 处理错误

  • 使用最终的 Promise.then 调用来模拟 finally

使用 Promise.resolve 开始一个 Promise 链

在本章前面的食谱中,我们一直在使用构造函数创建新的promise对象。这完成了工作,但同时也产生了一个问题。承诺链中的第一个回调与后续的回调形状不同。

在第一个回调中,参数是触发后续thencatch回调的resolvereject函数。在后续的回调中,返回值沿着链向下传播,抛出的错误被catch回调捕获。这种差异增加了心理负担。如果链中的所有函数都以相同的方式表现,那就太好了。

在这个食谱中,我们将看到如何使用Promise.resolve来启动承诺链。

准备工作

这个食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 3-05-starting-with-resolve的新文件夹。

  3. 复制或创建一个index.html,该文件从main.js加载并运行一个main函数。

  4. 创建一个名为main.js的文件,该文件使用空对象作为第一个参数调用Promise.resolve

export function main () { 
 Promise.resolve({}) 
} 
  1. resolve链中添加一个then调用,并将火箭助推器附加到传递的对象上:
export function main () { 
  Promise.resolve({}).then(function (rocket) {
 console.log('attaching boosters'); rocket.boosters = [{ count: 2, fuelType: 'solid' }, { count: 1, fuelType: 'liquid' }]; return rocket; })
} 
  1. 在链中添加一个最终的then调用,以便知道何时添加了boosters
export function main () { 
  Promise.resolve({}) 
    .then(function (rocket) { 
      console.log('attaching boosters'); 
      rocket.boosters = [{ 
        count: 2, 
        fuelType: 'solid' 
      }, { 
        count: 1, 
        fuelType: 'liquid' 
      }]; 
      return rocket; 
    }) 
 .then(function (rocket) { console.log('boosters attached'); console.log(rocket); })
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

Promise.resolve创建一个新的承诺,该承诺解析传递给它的值。后续的then方法将接收该解析值作为其参数。这个方法可能看起来有点绕,但可以非常有助于组合异步函数。实际上,承诺链的组成部分不需要意识到它们在链中(包括第一步)。这使得从不需要承诺的代码过渡到需要承诺的代码变得更加容易。

使用 Promise.all 解决多个承诺

到目前为止,我们已经看到了如何使用承诺(promises)来按顺序执行异步操作。当单个步骤是长时间运行的操作时,这很有用。然而,这并不总是更有效的配置。很多时候,我们可以同时执行多个异步操作。

在这个食谱中,我们将看到如何使用Promise.all来启动多个异步操作,而无需等待前一个操作完成。

准备工作

这个食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 3-06-using-promise-all的新文件夹。

  3. 复制或创建一个index.html,该文件从main.js加载并运行一个main函数。

  4. 创建一个main.js文件,创建一个名为rocket的对象,并将空数组作为第一个参数调用Promise.all

export function main() { 
  console.log('Before promise created'); 

  const rocket = {}; 
  Promise.all([]) 

  console.log('After promise created'); 
}  
  1. 创建一个名为addBoosters的函数,该函数将boosters对象添加到另一个对象中:
function addBoosters (rocket) { 
  console.log('attaching boosters'); 
  rocket.boosters = [{ 
    count: 2, 
    fuelType: 'solid' 
  }, { 
    count: 1, 
    fuelType: 'liquid' 
  }]; 
  return rocket;  
}   
  1. 创建一个名为performGuidanceDiagnostic的函数,该函数返回一个成功完成的任务的承诺:
function performGuidanceDiagnostic (rocket) { 
  console.log('performing guidance diagnostic'); 

  return new Promise(function (resolve) { 
    setTimeout(function () { 
      console.log('guidance diagnostic complete'); 
      rocket.guidanceDiagnostic = 'Completed'; 
      resolve(rocket); 
    }, 2000); 
  }); 
}    
  1. 创建一个名为loadCargo的函数,该函数向cargoBay添加有效载荷:
function loadCargo (rocket) { 
  console.log('loading satellite'); 
  rocket.cargoBay = [{ name: 'Communication Satellite' }] 
  return rocket; 
}  
  1. 使用Promise.resolve将这些函数中的rocket对象传递给Promise.all
export function main() { 

  console.log('Before promise created'); 

  const rocket = {}; 
 Promise.all([ Promise.resolve(rocket).then(addBoosters), Promise.resolve(rocket).then(performGuidanceDiagnostic), Promise.resolve(rocket).then(loadCargo) ]); 

  console.log('After promise created'); 
} 
  1. 将一个then调用附加到链中,并记录火箭已准备好发射:
  const rocket = {}; 
  Promise.all([ 
    Promise.resolve(rocket).then(addBoosters), 
    Promise.resolve(rocket).then(performGuidanceDiagnostic), 
    Promise.resolve(rocket).then(loadCargo) 
 ]).then(function (results) { console.log('Rocket ready for launch'); console.log(results); });
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

Promise.allPromise.resolve类似;参数被解析为承诺。区别在于,Promise.all接受一个可迭代参数,每个成员都单独解决。

在前面的示例中,你可以看到每个承诺都是立即启动的。其中两个在performGuidanceDiagnostic继续进行时已经完成。当所有组成部分的承诺都已解决时,Promise.all返回的承诺得到满足。

承诺的结果被组合成一个数组并向下传播。你可以看到三个对rocket的引用被打包到results参数中。你可以看到每个承诺的操作都已在对结果对象上执行。

还有更多

如你所猜,组成部分的承诺不需要返回相同的值。这可能在执行多个独立的网络请求时很有用。每个承诺的结果索引对应于Promise.all参数中操作的索引。在这些情况下,使用数组解构来命名then回调的参数可能很有用:

 Promise.all([ 
  findAstronomers, 
  findAvailableTechnicians, 
  findAvailableEquipment 
]).then(function ([astronomers, technicians, equipment]) { 
  // use results for astronomers, technicians, and equipment 
}); 

使用 Promise.catch 处理错误

在之前的配方中,我们看到了如何使用reject来满足具有错误状态的承诺,并看到这触发了承诺链中的下一个catch回调。由于承诺相对容易组合,我们需要能够处理以不同方式报告的错误。幸运的是,承诺能够无缝地处理这些问题。

在这个配方中,我们将看到Promises.catch如何处理通过抛出或拒绝报告的错误。

准备工作

此配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为3-07-handle-errors-promise-catch的新文件夹。

  3. 复制或创建一个index.html,该文件从main.js加载并运行一个main函数。

  4. 创建一个包含main函数的main.js文件,该函数创建一个名为rocket的对象:

export function main() { 

  console.log('Before promise created'); 

  const rocket = {}; 

  console.log('After promise created'); 
} 
  1. 创建一个会抛出错误的addBoosters函数:
function addBoosters (rocket) { 
  throw new Error('Unable to add Boosters'); 
} 
  1. 创建一个返回拒绝错误的performGuidanceDiagnostic函数:
function performGuidanceDiagnostic (rocket) { 
  return new Promise(function (resolve, reject) { 
    reject(new Error('Unable to finish guidance diagnostic')); 
  }); 
} 
  1. 使用Promise.resolve将这些函数传递火箭对象,并在每个函数后面链式添加一个catch
export function main() { 

  console.log('Before promise created'); 

  const rocket = {}; 
 Promise.resolve(rocket).then(addBoosters) .catch(console.error); Promise.resolve(rocket).then(performGuidanceDiagnostic)
 .catch(console.error);

  console.log('After promise created'); 
}  
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

正如我们之前看到的,当一个承诺在拒绝状态下被履行时,catch函数的回调会被触发。在先前的配方中,我们看到这会在调用reject方法时发生(例如,使用performGuidanceDiagnostic)。当链中的某个函数抛出错误时(例如addBoosters),也会发生这种情况。

这与Promise.resolve如何使异步函数正常化有类似的益处。这种处理允许异步函数不知道承诺链,并以一种对新手承诺的开发者来说熟悉的方式宣布错误状态。

这使得扩展承诺的使用变得更加容易。

使用承诺 API 模拟 finally

在先前的配方中,我们看到了如何使用catch来处理错误,无论是承诺被拒绝,还是回调抛出错误。有时,我们希望无论是否检测到错误状态都执行代码。在try/catch块的情况下,finally块可以用于此目的。当与承诺一起工作时,我们必须做更多的工作才能获得相同的行为。

在这个配方中,我们将看到如何通过承诺 API 模拟最终的then调用,以在成功和失败履行状态下执行一些代码。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为3-08-simulating-finally的新文件夹。

  3. 复制或创建一个index.html,它加载并运行来自main.jsmain函数。

  4. 创建一个包含main函数的main.js文件,该函数在承诺创建前后记录消息:

export function main() { 

  console.log('Before promise created'); 

  console.log('After promise created'); 
} 
  1. 创建一个名为addBoosters的函数,如果其第一个参数为false则抛出错误:
function addBoosters(shouldFail) { 
  if (shouldFail) { 
    throw new Error('Unable to add Boosters'); 
  } 

  return { 
    boosters: [{ 
      count: 2, 
      fuelType: 'solid' 
    }, { 
      count: 1, 
      fuelType: 'liquid' 
    }] 
  }; 
} 
  1. 使用Promise.resolve传递一个布尔值给addBoosters,如果随机数大于0.5则为true
export function main() { 

  console.log('Before promise created'); 

 Promise.resolve(Math.random() > 0.5) .then(addBoosters) 

  console.log('After promise created'); 
} 
  1. 在链中添加一个记录成功信息的then函数:
export function main() { 

  console.log('Before promise created'); 
  Promise.resolve(Math.random() > 0.5) 
    .then(addBoosters) 
 .then(() => console.log('Ready for launch: ![](https://github.com/OpenDocCN/freelearn-html-css-js-zh/raw/master/docs/es-cb/img/4a46c1bc-b445-489e-8cfc-b55eda1f0b5b.png)')) 

  console.log('After promise created'); 
} 
  1. 在链中添加一个catch,如果抛出错误则记录下来:
export function main() { 
  console.log('Before promise created'); 
  Promise.resolve(Math.random() > 0.5) 
    .then(addBoosters) 
    .then(() => console.log('Ready for launch: ')) 
 .catch(console.error) 
  console.log('After promise created'); 
} 
  1. catch之后添加一个then,并记录我们需要发布公告:
export function main() { 

  console.log('Before promise created'); 
  Promise.resolve(Math.random() > 0.5) 
    .then(addBoosters) 
    .then(() => console.log('Ready for launch: ')) 
    .catch(console.error)
    .then(() => console.log('Time to inform the press.')); 
  console.log('After promise created'); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 如果你运气好并且成功添加了助推器,你会看到以下输出:

图片

  1. 如果你运气不好,你会看到如下错误信息:

图片

它是如何工作的...

我们可以在前面的输出中看到,无论异步函数是否在错误状态下完成,最后的 then 回调都会被执行。这是可能的,因为 catch 方法不会停止承诺链。它只是捕获链中先前链接的任何错误状态,然后将新的值向前传播。

最后的 then 通过这个 catch 被保护,防止被错误状态绕过。因此,无论链中先前链接的履行状态如何,我们都可以确信这个最后的 then 回调将被执行。

第四章:使用 async/await 和函数进行操作

在本章中,我们将介绍以下食谱:

  • 使用异步函数创建 Promise

  • 等待异步函数的结果

  • 在 Promise 链中使用异步结果

  • 等待一系列中的多个结果

  • 并发等待多个结果

  • 使用 Promise.all 收集异步结果数组

  • 等待异步函数时处理错误

  • 处理 Promise.all 内部抛出的错误

  • 使用 finally 确保执行操作

简介

在上一章中,我们看到了 Promise 相比回调模式是一个巨大的改进。但我们还看到了在组合方面仍然存在一些粗糙的边缘。直接创建 Promise 需要不同的形状函数,这些函数被放置在链的后面。错误和成功的结果以不同的方式提供,这取决于 Promise 的创建方式。而且仍然有一些不便的嵌套。

asyncawait 操作符是在 ES8 中引入的。它们建立在 Promise 的基础上,使得处理和创建 Promise 更加流畅。在本章中,我们将看到如何使用 asyncawait 以更优雅的方式创建和处理 Promise。

使用异步函数创建 Promise

async 函数是创建和操作 Promise 的简单方法。在本食谱中,我们将看到这种基本形式。

准备工作

本食谱假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序,导航到你的 workspace。

  2. 创建一个名为 04-01-creating-Promise-with-async 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 加载并运行一个 main 函数。

  4. 创建一个包含名为 someTaskasync 函数的 main.js

// main.js 
async function someTask () { 
   console.log('Performing some task'); 
} 
  1. 创建一个调用 someTask 并在 someTask 执行前后记录信息的 main
export function main () { 
  console.log('before task'); 
  someTask(); 
  console.log('after task created'); 
}  
  1. someTask 上链式调用 then 并在回调函数中记录一条信息:
export function main () { 
  console.log('Before Promise created'); 
  someTask().then(function () { 
    console.log('After Task completed'); 
  }); 
  console.log('After Promise created');}  
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

async 关键字指示运行时该函数返回一个 Promise,而不是直接的结果。通过查看日志信息,你可以清楚地看到操作顺序。第一条信息是在调用 async 函数之前记录的。接下来,记录 async 函数内的信息。然后是调用 async 函数后的信息。最后,记录 then 回调函数内的信息。这个顺序与直接使用 Promise API 的代码中可看到的执行顺序相同。

之前的代码已经比直接创建承诺的代码有所改进。在接下来的示例中,我们将看到如何利用 await 从这些函数中检索结果,而不使用 Promise API。我们还将探讨其他情况下 async 函数如何比直接使用 Promise API 提供优势。

等待异步函数的结果

在上一个示例中,我们看到了如何使用 async 创建解析承诺的函数。然而,我们使用了 Promise API 的 then 回调来等待结果。在许多情况下,我们可以使用 await 关键字来等待这些值。这可以完成任务,但有一种更干净的方式来从异步函数中检索结果。

本示例演示了如何使用 await 来返回长时间运行操作的结果。

准备工作

本示例假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 4-02-await-async-results 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,其中包含一个名为 getRandomNumberasync 函数,该函数返回一个随机数:

// main.js 
async function getRandomNumber () { 
   return Math.random(); 
} 
  1. 创建一个名为 mainasync 函数,该函数调用 getRandomNumber,等待结果,并输出值:
export async function main () { 
  console.log('before task'); 
  const result = await getRandomNumber(); 
  console.log('Received the value: %s', result); 
  console.log('after task completed'); 
}  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

图片

工作原理...

async 函数中使用 await 等同于在 Promise 链中使用 then。区别在于,结果不是作为回调函数的参数传递,而是作为表达式解析。这个表达式可以被分配给常量 result。然后,这个值就可以在整个块中使用,而不仅限于回调函数的体内。

这非常酷!以前从异步代码中获取结果需要使用回调和 Promise API 中的方法。现在,有了 asyncawait 关键字,我们可以编写无需嵌套的代码,这使得代码更容易阅读和理解,同时保持与 Promise API 的兼容性。

在 Promise 链中使用异步结果

在上一个示例中,我们看到了如何使用 asyncawait 来替换 Promise API 的一部分。然而,仍然会有一些情况下使用 Promise API 更为可取,无论是为了清晰度、结构还是逐步替换。

在本示例中,我们将看到 async 函数如何无缝地集成到 Promise 链中。

准备工作

本示例假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 04-03-async-function-Promise-chain 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 getRandomNumberasync 函数,它返回一个随机数:

async function getRandomNumber() { 
  console.log('Getting random number.'); 
  return Math.random(); 
} 
  1. 创建一个名为 determinReadyToLaunchasync 函数,如果其第一个参数大于 0.5,则返回 true
async function deteremineReadyToLaunch(percentage) { 
  console.log('Determining Ready to launch.'); 
  return Math.random() > 0.5; 
}  
  1. 创建一个名为 reportResults 的第三个 async 函数,如果其第一个参数是 truefalse,则输出不同的结果:
async function reportResults(isReadyToLaunch) { 
  if (isReadyToLaunch) { 
    console.log('Rocket ready to launch. Initiate countdown: '); 
  } else { 
    console.error('Rocket not ready. Abort mission: '); 
  } 
}  
  1. 创建一个 main 函数,调用 getRandomNumber,并创建一个 Promise 链来依次调用 determineReadyToLaunchreportResults
export function main() { 
  console.log('Before Promise created'); 
  getRandomNumber() 
    .then(deteremineReadyToLaunch) 
    .then(reportResults) 
  console.log('After Promise created'); 
}   
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

它是如何工作的...

如前所述,async 函数使用 Promise 解析其结果,而不是直接返回一个值。这意味着,无论 async 函数是用于启动还是继续 Promise 链,其形状都可以是相同的。

还有更多...

事实上,因为结果总是通过 Promise 解析,所以可以使用 Promise.allasync 函数作为一个组来解析。你可以看到使用 Promise.allasync 函数及其结果连接的示例:

async function checkEngines(threshold = 0.9) { 
  return Math.random() < threshold; 
} 

async function checkFlightPlan(threshold = 0.9) { 
  return Math.random() < threshold; 
} 

async function checkNavigationSystem(threshold = 0.9) { 
  return Math.random() < threshold; 
} 

Promise.all([ 
    checkEngines(), 
    checkFlightPlan(0.5), 
    checkNavigationSystem(0.75) 
]).then(function([enginesOk, flighPlanOk, navigationOk]) { 
  if (enginesOk) { 
    console.log('engines ready to go'); 
  } else { 
    console.error('engines not ready'); 
  } 

  if (flighPlanOk) { 
    console.log('flight plan good to go'); 
  } else { 
    console.error('error found in flight plan'); 
  } 

  if (navigationOk) { 
    console.log('navigation systems good to go'); 
  } else { 
    console.error('error found in navigation systems'); 
  } 
}) 

上述代码按预期工作。函数甚至可以直接使用参数调用,而无需将它们包裹在 Promise.resolve 的调用中。

系列中等待多个结果

有时候有必要按顺序安排异步操作。在之前的菜谱中,我们看到了如何使用 Promise.then 来做这件事。在这个菜谱中,我们将看到如何使用 await 操作符来完成同样的任务。

准备工作

本菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 04-03-async-function-Promise-chain 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 async 函数,名为 getRandomNumber,它返回一个随机数:

async function getRandomNumber() { 
  console.log('Getting random number.'); 
  return Math.random(); 
} 
  1. 创建一个名为 determineReadyToLaunchasync 函数,如果其第一个参数大于 0.5,则返回 true
async function deteremineReadyToLaunch(percentage) { 
  console.log('Determining Ready to launch.'); 
  return Math.random() > 0.5; 
} 
  1. 创建第三个 async 函数,名为 reportResults,如果它的第一个参数是 truefalse,则输出不同的结果:
async function reportResults(isReadyToLaunch) { 
  if (isReadyToLaunch) { 
    console.log('Rocket ready to launch. Initiate countdown: '); 
  } else { 
    console.error('Rocket not ready. Abort mission: '); 
  } 
}  
  1. 创建一个 main 函数,调用 getRandomNumber,等待结果,将其传递给 determineReadyToLaunch,并在等待启动准备就绪后调用 reportResults
export async function main() { 
  const randomNumber = await getRandomNumber();
  const ready = await deteremineReadyToLaunch(randomNumber);
  await reportResults(ready); 
}  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出:

它是如何工作的...

因为我们的main函数被标记为async,所以我们能够在它的主体中使用await运算符。这个运算符使得函数等待表达式解析的结果。在我们的例子中,这意味着我们调用的async函数创建的 Promise 得到了满足。

一旦结果得到满足,控制流将继续到下一个语句。如果我们没有使用await运算符,randomNumber的值将是一个承诺,它将解析为返回的值。我们可以使用承诺接口来处理这个问题,但由于我们使用了await,我们能够编写出更类似于同步代码的代码。

参见

  • 同时等待多个结果

同时等待多个结果

有时可以同时启动多个异步操作。这可能是有益的,例如,如果需要多个网络请求来获取给定页面的所有数据。在开始下一个请求之前等待每个请求完成会浪费时间。

在这个配方中,我们将看到如何使用await来并发地启动和等待多个结果。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为04-05-await-concurrently的新文件夹。

  3. 创建三个函数,checkEnginescheckFlightPlancheckNavigationSystem,当它们开始时记录一条消息,并在某些超时后返回一个解析为truePromise,如果随机数高于阈值:

function checkEngines() { 
  console.log('checking engine'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('engine check completed'); 
      resolve(Math.random() < 0.9) 
    }, 250) 
  }); 
} 

function checkFlightPlan() { 
  console.log('checking flight plan'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('flight plan check completed'); 
      resolve(Math.random() < 0.9) 
    }, 350) 
  }); 
} 

function checkNavigationSystem() { 
  console.log('checking navigation system'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('navigation system check completed'); 
      resolve(Math.random() < 0.9) 
    }, 450) 
  }); 
}  
  1. 创建一个async作为main函数,该函数调用之前步骤中创建的每个函数。将每个函数返回的值分配给一个局部变量。然后等待 Promise 的结果,并输出结果:
  export async function main() { 
  const enginePromise = checkEngines(); 
  const flighPlanPromise = checkFlightPlan(0.5); 
  const navSystemPromise = checkNavigationSystem(0.75); 

  const enginesOk = await enginePromise; 
  const flighPlanOk = await flighPlanPromise; 
  const navigationOk = await navSystemPromise; 

  if (enginesOk && flighPlanOk && navigationOk) { 
    console.log('All systems go, ready to launch: '); 
  } else { 
    console.error('Abort the launch: '); 

    if (!enginesOk) { 
      console.error('engines not ready'); 
    } 

    if (flighPlanOk) { 
      console.error('error found in flight plan'); 
    } 

    if (navigationOk) { 
      console.error('error found in navigation systems'); 
    } 
  } 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

你将在输出中看到checking消息立即被记录。因为所有函数都是在第一次await使用之前被调用的,所以操作是在await时启动的。这允许在任何一个完成之前,所有超时都开始。

一旦使用awaitmain函数将阻塞,直到结果解析。代码再次同步。每个函数的结果按顺序解析,但代码的长时间运行部分(setTimeout)是并发的。

由于我们使用了setTimeout,三个检查函数必须手动返回 Promise。因为这个函数使用回调,所以我们不能使用async/await

使用 Promise.all 收集异步结果数组

在之前的菜谱中,我们看到了如何在等待结果之前触发多个异步函数。我们也看到了 Promise API 和 asyc/await 操作符是如何一起工作的。在某些情况下,使用 Promise API 是更可取的。

在这个菜谱中,我们将看到如何使用 Promise.all 来收集多个异步操作的结果。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 04-06-Promise-all-collect-concurrently 的新文件夹。

  3. 创建三个函数,checkEnginescheckFlightPlancheckNavigationSystem,当它们开始时记录一条消息,并在某些超时后返回一个解析为 truePromise,如果随机数高于阈值:

function checkEngines() { 
  console.log('checking engine'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('engine check completed'); 
      resolve(Math.random() < 0.9) 
    }, 250) 
  }); 
} 

function checkFlightPlan() { 
  console.log('checking flight plan'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('flight plan check completed'); 
      resolve(Math.random() < 0.9) 
    }, 350) 
  }); 
} 

function checkNavigationSystem() { 
  console.log('checking navigation system'); 

  return new Promise(function (resolve) { 
    setTimeout(function() { 
      console.log('navigation system check completed'); 
      resolve(Math.random() < 0.9) 
    }, 450) 
  }); 
}  
  1. 创建一个 async 作为 main 函数,调用之前步骤中创建的每个函数。使用 Promise.all 收集结果,将结果缩减为一个单一的 ok to launch 值,并记录结果:
export async function main() { 
  const prelaunchChecks = [ 
    checkEngines(), 
    checkFlightPlan(0.5), 
    checkNavigationSystem(0.75) 
  ]; 

  const checkResults = await Promise.all(prelaunchChecks); 
  const readyToLaunch = checkResults.reduce((acc, curr) => acc && 
  curr); 

  if (readyToLaunch) { 
    console.log('All systems go, ready to launch: '); 
  } else { 
    console.error('Something went wrong, abort the launch: '); 
  } 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出:

它是如何工作的...

Promise.all 返回一个解析为从多个值解析出的值的数组的一个 Promise。在我们的情况下,这些是我们创建在 步骤 3 中的异步函数。一旦创建了该 Promise,我们就可以等待结果。

一旦我们得到这个结果,我们可以使用 Array.prototype.reduce 方法来创建一个单一的布尔值,这个值可以用在条件语句中。

如果我们将这个菜谱与之前的菜谱进行比较,我们可以看到优势。添加另一个预启动检查就像向函数数组中添加另一个异步函数一样简单。实际上,我们不需要提前知道需要执行多少预启动检查。如果它们都解析为布尔值,它们将与 Promise.all 一起工作。我们失去了关于哪个步骤失败的信息,但我们将看到在未来的菜谱中如何通过错误处理来恢复这些信息。

更多...

有可能重构 main 函数,使得预检查函数可以隐式执行。我们可以使用 Array.prototype.map 函数来完成这个任务:

 export async function main() { 
  const prelaunchChecks = [ 
    checkEngines, 
    checkFlightPlan, 
    checkNavigationSystem 
  ]; 
 const checkResults = await Promise.all(prelaunchChecks.map((check) => 
  check()); 
  const readyToLaunch = checkResults.reduce((acc, curr) => acc && 
  curr); 

  if (readyToLaunch) { 
    console.log('All systems go, ready to launch: '); 
  } else { 
    console.error('Something went wrong, abort the launch: '); 
  } 

突出的部分显示了异步函数是在 map 中调用的。

参见

  • 使用 Array.reduce 转换数据

  • 处理 Promise.all 内部抛出的错误

等待异步函数时处理错误

到目前为止,在本章中,我们看到了如何处理成功完成的 async 函数。但是,正如我们所知,这并不总是情况。我们需要能够处理异步函数或它们调用的任何函数抛出的错误。

在这个菜谱中,我们将看到 try-catch 块如何处理由 async 函数抛出的错误。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 4-07- async-errors-try-catch 的新文件夹。

  3. 复制或创建一个 index.html,它从 main.js 加载并运行一个 main 函数。

  4. 创建一个 async 函数,addBoosters,它抛出一些错误:

async function addBoosters() { 
  throw new Error('Unable to add Boosters'); 
} 
  1. 创建一个 async 函数,performGuidanceDiagnostic,它也会抛出错误:
async function performGuidanceDiagnostic (rocket) { 
  throw new Error('Unable to finish guidance diagnostic')); 
} 
  1. 创建一个 async 作为 main 函数,该函数调用 addBostersperformGuidanceDiagnostic 并处理错误:
 export async function main() { 
    console.log('Before Check'); 

  try { 
    await addBosters(); 
    await performGuidanceDiagnostic(); 
  } catch (e) { 
    console.error(e); 
  } 
} 

  console.log('After Check'); 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

当我们从异步函数等待一个以错误状态满足的结果时,会抛出一个错误。为了继续程序,我们需要捕获这个错误。在前面的菜谱中,第一个异步函数抛出错误,因此第二个操作没有执行,然后在退出 try-catch 块之前将错误记录到控制台。

这与 Promise 链错误处理相比有优势,它使用与同步代码相同的 try-catch 机制来处理错误。我们不需要将同步代码包装在承诺中,以便它们可以与 Promise.catch 一起工作;我们可以使用语言级别的 try-catch 块。

在下一个菜谱中,我们将看到 try-catch 如何与多个并发操作的多异步操作一起工作。

处理 Promise.all 内抛出的错误

在先前的菜谱中,我们看到了如何使用 Promise.all 来收集多个异步函数的结果。在错误状态下,Promise.all 更加有趣。通常,当我们处理多个可能错误条件时,如果我们想显示多个错误消息,我们必须编写布尔逻辑日志。但在本菜谱中,我们将看到如何使用 Promise.alltry-catch 块来同时处理多个错误条件,而不需要复杂的布尔逻辑。

准备中

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 04-06-Promise-all-collect-concurrently 的新文件夹。

  3. 创建三个 async 函数,checkEnginescheckFlightPlancheckNavigationSystem,当它们开始时记录一条消息,如果随机数高于阈值则返回一个拒绝错误的 Promise,或者在超时后解析:

function checkEngines() { 
  console.log('checking engine'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Engine check failed')); 
      } else { 
        console.log('Engine check completed'); 
        resolve(); 
      } 
    }, 250) 
  }); 
} 

function checkFlightPlan() { 
  console.log('checking flight plan'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Flight plan check failed')); 
      } else { 
        console.log('Flight plan check completed'); 
        resolve(); 
      } 
    }, 350) 
  }); 
} 

function checkNavigationSystem() { 
  console.log('checking navigation system'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Navigation system check failed')); 
      } else { 
        console.log('Navigation system check completed'); 
        resolve(); 
      } 
    }, 450) 
  }); 
} 

  1. 创建一个 async 作为 main 函数,该函数调用上一步创建的每个函数。等待结果,并捕获并记录抛出的任何错误。如果没有错误抛出,则记录成功:
 export async function main() { 
  try { 
    const prelaunchChecks = [ 
      checkEngines, 
      checkFlightPlan, 
      checkNavigationSystem 
    ]; 
    await Promise.all(prelauchCheck.map((check) => check()); 
; 
    console.log('All systems go, ready to launch: '); 
  } catch (e) { 
    console.error('Aborting launch: '); 
    console.error(e); 
  } 
   }    
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

正如我们之前看到的,Promise.all返回一个解析为多个值数组的 Promise,并且当它们被解析时我们可以await这些值。当我们await一个在错误状态下解析的 Promise 时,会抛出一个异常。

前面代码有趣的地方在于三个异步承诺正在并发执行。如果其中之一或多个以错误状态完成,那么将抛出一个或多个错误。

你会注意到只有一个错误被捕获并记录。与同步代码一样,我们的代码可能抛出多个错误,但只有一个是被catch块捕获并记录的。

使用finally确保操作执行

错误处理可能相当复杂。可能存在你想要允许错误继续向上冒泡到调用堆栈以在更高级别处理的情况。在这些情况下,你可能还需要执行一些清理任务。通常这意味着重置一些共享资源,但也可能是简单地记录应用程序的当前状态。

在这个菜谱中,我们将看到如何使用finally来确保某些代码无论错误状态如何都会被执行。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为04-06-Promise-all-collect-concurrently的新文件夹。

  3. 创建三个async函数,checkEnginescheckFlightPlancheckNavigationSystem,当它们开始时记录一条消息,并返回一个Promise,如果随机数高于阈值则拒绝错误,或者在超时后解析:

 function checkEngines() { 
  console.log('checking engine'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Engine check failed')); 
      } else { 
        console.log('Engine check completed'); 
        resolve(); 
      } 
    }, 250) 
  }); 
} 

function checkFlightPlan() { 
  console.log('checking flight plan'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Flight plan check failed')); 
      } else { 
        console.log('Flight plan check completed'); 
        resolve(); 
      } 
    }, 350) 
  }); 
} 

function checkNavigationSystem() { 
  console.log('checking navigation system'); 

  return new Promise(function (resolve, reject) { 
    setTimeout(function () { 
      if (Math.random() > 0.5) { 
        reject(new Error('Navigation system check failed')); 
      } else { 
        console.log('Navigation system check completed'); 
        resolve(); 
      } 
    }, 450) 
  }); 
}  
  1. 创建一个asyncperformCheck函数,调用之前步骤中创建的每个函数。等待结果,并使用finally记录一条完整的消息:
async function performChecks() { 
  console.log('Starting Pre-Launch Checks'); 
  try { 
    const prelaunchChecks = [ 
      checkEngines, 
      checkFlightPlan, 
      checkNavigationSystem 
    ]; 

    return Promise.all(prelauchCheck.map((check) => check()); 

  } finally { 
    console.log('Completed Pre-Launch Checks'); 
  } 
   }  
  1. 创建一个async作为main函数,调用performCheck函数。等待结果,使用try-catch处理任何错误,并记录发射是否可以继续:
export async function main() { 
  try { 
    await performChecks(); 
    console.log('All systems go, ready to launch: '); 
  } catch (e) { 
    console.error('Aborting launch: '); 
    console.error(e); 
  } 
   }  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

正如前面的菜谱中所示,错误被main函数捕获,并显示发射继续或中止的消息。在这个菜谱中,我们将check函数组合成一个单独的异步函数performChecks,这样我们就可以知道它们何时全部完成。

因为performChecks在等待的 Promise 结果上没有catch块,所以调用堆栈中较低级别的错误会冒泡到main函数。然而,finally块确保有一个消息让我们知道performChecks已经完成。

你可以想象这个组织可以扩展到包含多个层级和其他运营分支。在大型程序中处理错误是一项重要任务,而async/await允许我们使用try-catch块以相同的方式处理异步和同步代码中的错误。

第五章:Web Workers、共享内存和 Atomics

在本章中,我们将涵盖以下食谱:

  • 使用 Web Workers 在单独的线程上执行工作

  • 向 Web Workers 发送和接收消息

  • 向 Web Workers 发送数据

  • 使用 terminate 停止工作线程

  • 创建 SharedArrayBuffer

  • 将 SharedArrayBuffer 发送到 Web Worker

  • 从多个 Web Workers 读取 SharedArray

  • 使用 Atomics 协调共享内存的使用

  • 使用承诺为工作提供一个简单的接口

简介

每天 JavaScript 和 Web 应用程序的能力和期望都在不断扩大。这种扩展最令人兴奋的领域之一是并行编程,它与异步和并发编程相关,但并不相同。并行编程允许同时进行多个操作,而不是交错进行。

这种区别可能看起来很小,但意义重大。在本章中,我们将探讨如何使用网络平台上的工具来创建并行执行程序。我们将使用 Web Workers 来创建并行任务,SharedMemoryBuffer 来共享信息,以及 Atomic API 来协调它们之间的工作。

在 Firefox 中启用 SharedArrayBuffers

2018 年初,发现了 Spectre 和 Meltdown 漏洞。作为回应,浏览器制造商默认禁用了 SharedArrayBuffer。本章中的一些食谱需要此功能。本食谱演示了如何在 Firefox 中启用它们。

准备工作

本食谱假设您已安装了最新版本的 Firefox。

如何操作...

  1. 打开 Firefox。

  2. 导航到 about:config

  3. 点击“我接受风险!”

  4. 搜索“shared”。

  5. 双击 javascript.options.shared_memory

  6. 此选项现在应具有值 true:

如何工作...

默认情况下,Firefox 中禁用了共享内存,但选项允许开发者在不向普通用户暴露的情况下激活这些(可能不安全)的功能。您可以在以下链接中了解更多关于 Meltdown 和 Spectre 的信息:

meltdownattack.com/.

在您完成实验后,不应保留此功能启用。

在 Chrome 中启用 SharedArrayBuffers

2018 年初,发现了 Spectre 和 Meltdown 漏洞。作为回应,浏览器制造商默认禁用了 SharedArrayBuffer。本章中的一些食谱需要此功能。本食谱演示了如何在 Chrome 中启用它们。

准备工作

本食谱假设您已安装了最新版本的 Chrome。

如何操作...

  1. 打开 Chrome。

  2. 导航到 chrome://flags/

  3. 点击“我接受风险!”

  4. 搜索“shared”。

  5. 将选项“实验性启用 JavaScript 中的 SharedArrayBuffer 支持”设置为“启用”。

  6. 点击“立即重新启动”:

如何工作...

默认情况下,Firefox 中禁用了共享内存,但选项允许开发者在不向普通用户暴露的情况下激活这些(可能不安全)功能。你可以在以下位置了解更多关于 Meltdown 和 Spectre 的信息:

meltdownattack.com/.

在你完成实验后,不应启用此功能。

使用 Web Workers 在单独的线程上执行工作

Web Workers 允许浏览器操作在主线程之外进行。一旦创建,线程间的通信通过传递消息来实现。在这个示例中,我们将看到如何创建一个非常简单的工作者,并从主线程向其发送消息。

准备工作

这个示例假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为05-01-performing-work-with-web-workers的新文件夹。

  3. 复制或创建一个名为index.html的文件,该文件加载并运行来自main.jsmain函数。

  4. 创建一个包含main函数的main.js文件,该函数从一个名为worker.js的文件创建工作者。然后向worker发送一个类型为hello-message的消息:

// main.js 
export function main() {  
  console.log('Hello, from main.'); 
  const worker = new Worker('./worker.js'); 
  worker.postMessage({ type: 'hello-message' }); 
}  
  1. 创建一个名为worker.js的文件,该文件记录一个Hello消息:
// worker.js 
console.log('Hello, from the worker.');  
  1. worker.js文件中,在全局作用域上设置onmessage回调函数。这个函数应该记录接收到的消息的类型:
// worker.js 
console.log('Hello, from the worker.'); 

this.onmessage = function (message) {
 console.log('Message Recieved: (%s)', message.data.type); }
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

工作原理...

使用名为工作文件名的工作者名称创建工作者将在新线程上创建一个工作者。通过在工作者全局上下文中设置onmessage事件监听器,我们能够从主线程接收消息。

创建工作者后,main函数可以使用该引用向新工作者发送消息。postMessage方法的参数作为接收到的message属性的data属性传递给工作者。

向 Web Workers 发送和接收消息

在前面的示例中,我们看到了如何创建并向后台线程上的工作者发送消息。这非常棒!在 Web Workers 之前引入之前,JavaScript 无法与主线程以外的任何东西一起工作。然而,如果我们无法获取任何信息,这并不是很有用。

在这个示例中,我们将看到如何等待 Web Worker 的响应,并从 Web Worker 发送响应。

准备工作

这个示例假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为05-02-send-messages-to-and-from-web-workers的新文件夹。

  3. 复制或创建一个名为index.html的文件,该文件加载并运行来自main.jsmain函数。

  4. 创建一个名为 main.js 的文件,其中包含一个名为 onMessage 的函数,该函数接受一个名为 message 的参数并输出 typeindex 属性:

// main.js 
function onMessage(message) { 
  const { type, index } = message.data; 
  console.log('Main recieved a messge (%s) from index: (%s)',  
              type, index); 
}  
  1. 设置 WORKER_COUNT 常量:
// main.js 
const WORKER_COUNT = 5; 
  1. 创建一个 main 函数,该函数创建 WORKER_COUNT 个工作器,设置 onMessage 属性,并将 index 发送到工作器:
export function main() { 
  for (let index = 0; index < WORKER_COUNT; index++) { 
    const worker = new Worker('./worker.js'); 

    worker.onmessage = onMessage; 
    worker.postMessage({ type: 'ping', index }); 
  } 
}  
  1. 创建一个 worker.js 文件,将当前上下文分配为 global 常量:
// worker.js 
const global = this;  
  1. 在全局上下文中将 onmessage 事件监听器设置为 global。该函数应接受一个消息参数并输出 indextype 属性。然后它应该调用 global.postMessage 并传递其 index
// worker.js 
global.onmessage = (message) => { 
  const { type, index } = message.data; 
  console.log('Worker (%s) recieved a messge (%s)', index, type); 

  global.postMessage({ index, type: 'pong' }) 
  global.postMessage({ index, type: 'another-type' }) 
}; 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出显示:

图片

它是如何工作的...

我们已经看到如何使用 onmessage 监听器来监听从主线程发布的消息。现在我们可以看到,当在 main 函数中的工作器引用上绑定时,相同的监听器可以监听来自工作器的消息。我们还可以看到,这个监听器与被引用的个别工作器是隔离的。每个工作器都有一个独特的事件监听器;这对于组织工作器之间的通信很有用。

向 Web Worker 发送数据

现在我们已经看到如何发送和接收消息,我们可以开始真正使用这些 Web Workers。在这个配方中,我们将看到你可以向 Web Worker 发送和接收数据。

准备工作

此配方假设你已经有了一个工作区,允许你在浏览器中创建和运行 ES 模块。如果没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 05-03-send-data-to-and-from-web-workers 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,其中包含一个名为 onMessage 的函数,该函数接受一个名为 message 的参数并输出 typeindex 属性:

// main.js 
function onMessage(message) { 
  const { result, type } = message.data; 
  console.log('Result for operation (%s): (%s)', type, result); 
}  
  1. 创建一个 main 函数,该函数创建一个工作器,设置 onMessage 属性,创建一个随机数字数组,并向工作器发送两条消息,一条用于求和某些数字,另一条用于计算平均值:
export function main() { 
  const worker = new Worker('./worker.js'); 
  worker.onmessage = onMessage; 

  const numbers = (new Array(100)).fill().map(Math.random) 
  worker.postMessage({ type: 'average', numbers}); 
}  
  1. 创建一个 worker.js 文件,将当前上下文分配为 global 常量:
// worker.js 
const global = this; 
  1. global 上设置 onmessage 事件监听器,该函数应接受一个消息参数并对 message.datanumbers 属性执行求和或平均操作:
// worker.js 
global.onmessage = (message) => { 
  const { type, numbers } = message.data; 

  switch (type) { 
    case 'sum': 
      const sum = numbers.reduce((acc, curr) => acc + curr, 0); 
      global.postMessage({ result: sum, type }) 
      break; 
      case 'average': 
      const average = numbers.reduce((acc, curr) => acc + curr, 
       0) /numbers.length; 
      global.postMessage({ result: average, type }) 
      break; 
  } 
};  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

图片x

它是如何工作的...

我们已经看到了如何在工作器之间发送和接收简单的字符串。现在我们可以看到,更复杂的对象也可以发送。实际上,可以通过 postMessage 传递大量类型的对象。

要查看有效类型的完整列表,请访问以下链接:

developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm.

通过使用postMessage,我们已经将数据复制到了工作线程。这个操作成本较高,因为每次将消息发布到新线程时,数据都必须被复制才能可用。对于小型数据集,这不是问题,但对于大型数据集,它可能会很慢且占用大量内存。在本书的后续菜谱中,我们将使用共享内存来避免这种复制。

使用terminate停止工作线程

并非所有问题都是累积的。有些有一个期望的目标状态;一旦找到,程序就可以退出。我们已经看到,工作线程通过发送消息来传达结果。现在我们的程序已经完成,我们希望防止未来的消息被接收,以免污染我们的结果。

在这个菜谱中,我们将看到如何使用Worker.terminate立即停止一个Worker

准备中

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为05-04-stop-workers-with-terminate的新文件夹。

  3. 复制或创建一个index.html文件,该文件从main.js加载并运行一个main函数。

  4. 创建一个main.js文件,其中包含一个名为onMessage的函数,该函数接受messageworkers参数记录message.datatypetimeout属性,并对所有workers调用terminate

// main.js 
function onComplete(message, workers) { 
  const { index, timeout } = message.data; 
  workers.map((w) => w.terminate()); 

  console.log( 
    'Result from worker (%s) after timeout (%s): %s', 
    index, 
    timeout 
  ); 
}  
  1. 创建一个main函数,该函数创建多个工作线程,将带有函数的onMessage属性设置为将所有workers作为第二个参数传递,然后向每个工作线程发送带有index的消息:
  export function main() { 
  const totalWorkers = 10; 
  const workers = []; 

  for (let i = 0; i < totalWorkers; i++) { 
    const worker = new Worker('./worker.js'); 
    worker.onmessage = (msg) => onComplete(msg, workers); 
    workers.push(worker); 
  } 

  workers.map((worker, index) => { 
    workers[index].postMessage({ index }); 
  }); 
}  
  1. 创建一个worker.js文件,将当前上下文作为global常量:
// worker.js 
const global = this;  
  1. 设置一个timeout常量,其值在010,000之间的随机数:
// worker.js 
const timeout = Math.floor(Math.random() * 10000); 
  1. global上下文中设置onmessage事件监听器。该函数应接受一个message参数,并在给定的超时后发送一个包含此工作线程的indextimeout的响应消息:
// worker.js 
global.onmessage = (message) => { 
  const data = JSON.parse(message.data); 
  const data = { 
    index: data.index, 
    timeout: timeout 
  }; 

  setTimeout(() => global.postMessage(data), timeout) 
};   
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

它是如何工作的...

所有 10 个工作线程都被指示在给定的超时后发送消息,但正如你所看到的,只有一个工作线程实际上向主线程发送了消息。这是因为,在这个第一个工作线程发送消息后,我们对所有工作线程调用了terminate。这意味着它们将立即停止,除非另一个工作线程已经发送了消息,否则它永远不会发送。所以,我们几乎总是看到只有一个消息被发送。有可能在终止之前,一两个其他工作线程会发送消息。

创建 SharedArrayBuffer

到目前为止,我们已经看到了如何在主线程和工作线程之间发送数据。我们目前所做的方法的缺点是数据被复制。这意味着随着数据量和工作者数量的增加,需要复制的数量也会增加。幸运的是,有一种方法可以在线程之间以更少的开销共享数据。

SharedArrayBuffer 可以在数组之间共享,而不需要复制数据。在本例中,我们将看到如何创建、读取和写入 SharedArrayBuffer 中的数据。

准备工作

本例假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 05-05-creating-shared-array-buffer 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个包含主方法的 main.js 文件,该方法定义了 NUM_COUNTBYTES_FOR_32_BITARRAY_SIZEMAX_NUMBER 的常量:

export function main() { 
  const NUM_COUNT = 2048; 
  const BYTES_FOR_32_BIT = 4; 
  const ARRAY_SIZE = NUM_COUNT * BYTES_FOR_32_BIT; 
  const MAX_NUMBER = 1024; 
} 
  1. 接下来,创建一个大小为 ARRAY_SIZESharedArrayBuffer,并创建一个将其转换为 Int32Array 的实例:
export function main() { 
  // ... 
 const sab = new SharedArrayBuffer(ARRAY_SIZE);
 const intBuffer = new Int32Array(sab);
}  
  1. 用介于 0MAX_NUMBER 之间的随机数填充 intBuffer
export function main() { 
  // ... 
 // fill with random numbers  // fill with random numbers
  intBuffer.forEach((value, index) => {
    intBuffer[index] = Math.random() * MAX_NUMBER;
  }); } 
  1. 计算并打印数组中值的总和:
 export function main() { 
  // ... 
 // sum the ints
  const sum = intBuffer.reduce((acc, number) =>
    acc + number
  , 0);
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

在 JavaScript 中工作时考虑字节大小似乎有些不自然,但与 SharedArrayBuffer 一起工作却是必要的。前面的示例创建了一个包含 2,048 个 32 位整数的数组。因此,为了创建 SharedArray 缓冲区,我们必须计算 2,048 个 32 位整数将占用多少内存。这是通过简单的乘法完成的。一旦我们有了 ARRAY_SIZE,我们就可以分配内存。

现在我们有了数组,我们需要将其转换为某种类型,以便从中读取和写入。我们使用 Int32Array,因此当我们执行数组访问操作时,值将被转换为 32 位整数。

当填充数组时,我们只需遍历每个数字,将随机数乘以 MAX_NUMBER;得到的结果被转换为 32 位整数(十进制值丢失)。接下来,使用数组的 reduce 函数进行求和,并将结果输出。

Int32Array 的一个优点是它具有所有数组方法。因此,我们可以使用新数据类型执行 map、join、index、includes 等操作。

将 SharedArrayBuffer 发送到 Web Worker

现在我们知道了如何创建和使用 SharedArrayBuffer,我们可以使用它来在主线程和工作线程之间共享数据。如前所述,这比发布 JavaScript 对象有优势,因为数据不需要复制;它是共享的。

在这个菜谱中,我们将看到如何与工作线程共享SharedArrayBuffer,并将结果回传到主线程。

准备工作

本菜谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。它还假设您已在浏览器中启用了共享内存。如果您还没有,请参阅本章开头的菜谱。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的工作区。

  2. 创建一个名为05-06-sending-shared-array-to-worker的新文件夹。

  3. 复制或创建一个index.html,它加载并运行来自main.jsmain函数。

  4. 创建一个包含onMessage函数的main.js文件,该函数将接收到的消息数据的resulttype成员打印出来:

function onMessage(message) { 
  const { result, type } = message.data; 
  console.log('result from worker operation: %s', type, result); 
} 
  1. 创建一个包含主方法的main.js文件,定义NUM_COUNTBYTES_FOR_32_BITARRAY_SIZEMAX_NUMBER常量:
export function main() { 
  const NUM_COUNT = 2048; 
  const BYTES_FOR_32_BIT = 4; 
  const ARRAY_SIZE = NUM_COUNT * BYTES_FOR_32_BIT; 
  const MAX_NUMBER = 1024; 
} 
  1. 接下来,创建一个大小为ARRAY_SIZESharedArrayBuffer,并使用worker.js中的源创建一个工作线程:
export function main() { 
  // ... 
 const sab = new SharedArrayBuffer(ARRAY_SIZE); const worker = new Worker('./worker.js'); 
} 
  1. 将工作线程的消息事件监听器设置为onMessage函数,并向工作线程发送包含数组缓冲区的消息:
export function main() { 
  // ... 
 worker.onmessage = onMessage; worker.postMessage({ type: 'load-array', array: sab });; 
} 
  1. 使用介于0MAX_NUMBER之间的随机值填充数组缓冲区以 32 位整数:
export function main() { 
  // ... 
 const intBuffer = new Int32Array(sab); // fill with random numbers intBuffer.forEach((value, index) => { intBuffer[index] = Math.random() * MAX_NUMBER; }); 
} 
  1. 向工作线程发送消息,请求计算sumaverage
export function main() { 
  // ... 
 worker.postMessage({ type: 'calculate-sum' }); worker.postMessage({ type: 'calculate-average'}); 
} 
  1. 创建一个worker.js文件,将当前上下文赋值给变量global,声明一个名为sharedIntArray的变量,并将一个函数赋值给onmessage事件:
// worker.js 
const global = this; 
let sharedIntArray; 

global.onmessage = (message) => {};  
  1. onmessage监听器中获取message参数的数据组件,并switchtype属性上:
global.onmessage = (message) => { 
 const { data } = message; switch (data.type) {} 
};  
  1. 添加一个'load-array'的情况,我们将数据数组的属性转换为Int32Array后赋值给sharedIntArray
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
 case 'load-array': sharedIntArray = new Int32Array(data.array); break; 
    } 
  }; 
  1. 添加一个'calculate-sum'的情况,计算数组中所有数字的总和并将结果回传到主线程:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
    case 'load-array': 
      sharedIntArray = new Int32Array(data.array); 
      break; 
 case 'calculate-sum': const sum = sharedIntArray.reduce((acc, number) => acc +
      number, 
      0); global.postMessage({ type: 'sum', result: sum }); break; 
    } 
  };  
  1. 添加一个'calculate-average'的情况,计算数组中所有数字的平均值并将结果回传到主线程:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
    case 'load-array': 
      sharedIntArray = new Int32Array(data.array); 
      break; 
    case 'calculate-sum': 
      const sum = sharedIntArray.reduce((acc, number) =>  
        acc + number, 
      0); 
      global.postMessage({ type: 'sum', result: sum }); 
      break;     

 case 'calculate-average': const total = sharedIntArray.reduce((acc, number) => acc + number , 0); const average = total / sharedIntArray.length; global.postMessage({ type: 'average', result: average }); break; 
    } 
  }; 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:http://localhost:8000/

  2. 您应该看到以下输出:

工作原理...

如前所述,SharedArrayBuffer在线程之间不是复制的,它是共享的。因此,当我们创建共享数组缓冲区并将该引用传递给工作线程时,主线程插入到数组中的值在工作线程中也是可用的。

在工作线程接收到执行计算的指令后,值可以像任何其他数组一样累积,回传的值是一个简单的消息。

从多个 Web Workers 读取 SharedArray

在前面的菜谱中,我们看到了如何在主线程和一个单独的工作器之间共享数据。这对于在主线程上执行长时间运行的操作很有帮助,这有助于保持用户界面的响应性。然而,它并没有充分利用并行处理。对于非常大的数据集,在许多工作器之间分割计算可能是优势。

在这个菜谱中,我们将看到如何使用多个工作器生成结果的部分。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 05-07-reading-shared-buffer-from-multiple-workers 的新文件夹。

  3. 复制或创建一个 index.html,它加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,其中包含一个 onMessage 函数,用于记录消息数据中的以下成员:workenIndextyperesultworkerIndexstartIndexendIndexwindowSize

// main.js
function onMessage(message) {
const { 
    type, 
    result, 
    workerIndex, 
    startIndex, 
    endIndex, 
    windowSize 
  } = message.data; 
  console.log(`Result from worker operation { 
    type: ${type}, 
    result: ${result}, 
    workerIndex: ${workerIndex}, 
    startIndex: ${startIndex}, 
    endIndex: ${endIndex}, 
    windowSize: ${windowSize} 
  }`); 
} 
  1. 创建一个名为 main.js 的文件,其中包含一个主方法,用于定义 NUM_COUNTBYTES_FOR_32_BITARRAY_SIZEWORKER_COUNTMAX_NUMBER 的常量:
export function main() { 
  console.log('Main function starting.'); 
  const NUM_COUNT = 2048; 
  const BYTES_FOR_32_BIT = 4; 
  const ARRAY_SIZE = NUM_COUNT * BYTES_FOR_32_BIT; 
  const MAX_NUMBER = 32; 
  const WORKER_COUNT = 10; 
  } 
  1. 接下来,创建一个大小为 WORKER_COUNT 的工作器数组:
export function main() { 
  // ... 
 // create workers let workers = []; console.log('Creating workers.'); for (let i = 0; i < WORKER_COUNT; i++) { const worker = new Worker('./worker.js'); worker.onmessage = onMessage; workers = workers.concat(worker); } 
} 
  1. 接下来,创建一个大小为 ARRAY_SIZESharedArrayBuffer,并用随机整数填充它:
export function main() { 
  // ... 
 // create buffer and add data const sab = new SharedArrayBuffer(ARRAY_SIZE); const intBuffer = new Int32Array(sab); // fill with random numbers console.log('Filling Int buffer'); intBuffer.forEach((value, index) => { intBuffer[index] = (Math.random() * MAX_NUMBER) + 1; }); 
} 
  1. 将这些消息发布到每个工作器:'load-array''load-indices''calculate-sum''calculate-average'
export function main() { 
  // ... 
 workers.forEach((worker, workerIndex) => { worker.postMessage({ type: 'load-array', array: sab }); worker.postMessage({ type: 'load-indices', workerIndex,
    workerCount: WORKER_COUNT }); worker.postMessage({ type: 'calculate-sum' }); worker.postMessage({ type: 'calculate-average' }); });; 
} 
  1. 创建一个 worker.js 文件,将当前上下文赋值给变量 global,并声明名为 sharedIntArraysharedInArraySliceworkerIndexworkerCountstartIndexendIndex 的变量。此外,将一个函数赋值给 onmessage 事件:
// worker.js 
const global = this; 
let sharedIntArray; 
let sharedIntArraylSlice; 
let workerIndex; 
let workerCount; 
let startIndex; 
let endIndex; 

global.onmessage = (message) => {};  
  1. onmessage 监听器中,获取 message 参数的数据组件并基于 type 属性进行切换:
global.onmessage = (message) => { 
 const { data } = message; switch (data.type) {} };  
  1. 添加一个针对 'load-array' 的用例,在该用例中,我们将数据数组的属性转换为 Int32Array 后赋值给 sharedIntArray
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
 case 'load-array': sharedIntArray = new Int32Array(data.array); break; 
    } 
  };  
  1. 添加一个针对 'load-indices' 的用例,根据当前索引和总工人数计算当前工作器应处理的值窗口:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
    case 'load-array': 
      sharedIntArray = new Int32Array(data.array); 
      break; 
 case 'load-indices':
 workerIndex = data.workerIndex;
 workerCount = data.workerCount;

 const windowSize = Math.floor(sharedIntArray.length /
      workerCount)
 startIndex = windowSize * workerIndex;
 const isLastWorker = workerIndex === workerCount - 1;
 endIndex = (isLastWorker) ? sharedIntArray.length : 
      startIndex+windowSize;
 sharedIntArraySlice = sharedIntArray.slice(startIndex,
      endIndex);
 break;
  }; 
  1. 添加一个针对 'calculate-sum' 的用例,该用例计算数组中的所有数字并将结果回传到主线程:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
   // ... 
 case 'calculate-sum':
      const sum = sharedIntArraySlice.reduce((acc, number) =>
        acc + number
      , 0);
      sendResult('sum', sum);
      break; 
    } 
  };  
  1. 添加一个针对 'calculate-average' 的用例,该用例计算数组中的所有数字并将结果回传到主线程:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
    //...     
    case 'calculate-average':
      const total = sharedIntArraySlice.reduce((acc, number) =>
        acc + number
      , 0);
      const average = total / sharedIntArraySlice.length
      sendResult('average', average);
      break;     
    } 
  };  
  1. 创建一个 sendResult 函数,用于将 result、结果类型和有关当前线程的信息发布到主线程:
function sendResult(type, result) { 
  global.postMessage({ 
    type, 
    result, 
    workerIndex, 
    startIndex, 
    endIndex, 
    windowSize: endIndex - startIndex - 1 
  }); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该看到以下输出:

图片

工作原理...

共享数组缓冲区可以在任何数量的工作线程之间共享。为了计算结果的部分,我们需要在线程之间公平地分配结果。这就是工作线程 onMessage 监听器的 'load-indices' 情况所做的事情。

我们可以使用数组的大小和总工作线程数来获取窗口大小。我们取整数部分,这样就不会超过数组的索引。这也是因为不能有部分索引:它们是整数。

接下来,我们使用当前工作线程的索引来获取 startIndex(基本上是从上一个工作线程离开的地方继续)。为了获取结束索引,我们需要知道这是否是最后一个工作线程。如果不是,我们使用窗口大小。如果是,我们需要取所有剩余的值。

一旦我们有了数组切片,每个工作线程就像处理整个数组一样计算这些部分的和与平均值。然后,将结果和工作线程信息发布到主线程。

使用 Atomics 协调共享内存的使用

在上一个菜谱中,我们使用了多个工作线程来生成结果的一部分。我们可以在主线程中合并这些结果。虽然这种方法是有效的,但它并没有充分利用并行处理的优势。如果工作线程能够自己累积结果,那就更好了。

在多个并行线程中修改共享内存会暴露出竞争条件。这是当几个操作需要按照特定顺序发生,而这种顺序没有被强制执行时。幸运的是,我们可以使用 Atomics API 来协调这些操作。

在这个菜谱中,我们将看到如何使用 Atomics API 来累积结果,同时避免竞争条件。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 05-08-use-atomics-to-coordinate 的新文件夹。

  3. 复制或创建一个 index.html,它加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件。创建三个共享数组缓冲区:一个输入缓冲区和两个输出缓冲区(一个 safe,另一个 unsafe)。输出缓冲区的大小应为 32 位:

// main.js
const NUMBER_COUNT = Math.pow(2, 10); 
const BYTES_FOR_32_BIT = 4;
const ARRAY_SIZE = NUMBER_COUNT * BYTES_FOR_32_BIT; 
const sab = new SharedArrayBuffer(ARRAY_SIZE); 
const intBuffer = new Int32Array(sab); 
const outSab = new SharedArrayBuffer(BYTES_FOR_32_BIT); 
const unsafeSab = new SharedArrayBuffer(BYTES_FOR_32_BIT); 
const workerCount = 256; 
  1. 声明一个变量 workersFinishedCount 并将其设置为 0
// main.js
let workersFinishedCount = 0; 
  1. 创建一个名为 onMessage 的函数。这个函数增加 workersFinished。如果所有工作线程都已完成,则记录两个输出数组的结果,并记录本地计算的求和结果:
//main.js
function onMessage(message) { 
  workersFinishedCount++;  
  if (workersFinishedCount === WORKER_COUNT) { 
    const outIntBuffer = new Int32Array(outSab); 
    const unsafeIntBuffer = new Int32Array(unsafeSab); 
    console.log('Unsafe Sum: %s', unsafeIntBuffer[0]); 
    console.log('Merged sum: %s', outIntBuffer[0]); 
    const localSum = intBuffer.reduce((acc, curr) => acc + curr, 
    0); 
    console.log('Local sum: %s', localSum); 
  } 
} 
  1. 创建一个主函数,声明一个 MAX_NUMBER
export function main() { 
  const MAX_NUMBER = 32; 
} 
  1. 接下来,创建一个大小为 WORKER_COUNT 的工作线程数组。
export function main() { 
  // ... 
 // create workers
</strong>  let workers = [];
 console.log('Creating workers.'); for (let i = 0; i < WORKER_COUNT; i++) { const worker = new Worker('./worker.js'); worker.onmessage = onMessage; workers = workers.concat(worker); } 
} 
  1. 接下来,将输入共享数组缓冲区填充随机整数:
export function main() { 
  // ... 
 // fill with random numbers console.log('Filling Int buffer'); intBuffer.forEach((value, index) => { intBuffer[index] = (Math.random() * MAX_NUMBER) + 1; }); 
} 
  1. 将这些消息发送给每个工作线程:'load-shared-input''load-shared-output''load-indices''calculate-sum'
export function main() { 
  // ... 
 workers.forEach((worker, workerIndex) => { worker.postMessage({ type: 'load-shared-input', input: sab 
    }); worker.postMessage({ type: 'load-shared-output', safe: 
    outSab,
    unsafe: unsafeSab }); worker.postMessage({ type: 'load-indices', workerIndex, 
    workerCount: WORKER_COUNT }); worker.postMessage({ type: 'calculate-sum' }); }); 
} 
  1. 创建一个 worker.js 文件,将当前上下文分配给一个 global 变量,声明名为 sharedIntArrayresultArrayunsafeResultArraysharedInArraySlice 的变量,并将一个函数分配给 onmessage 事件:
// worker.js 
const global = this; 
let sharedIntArray; 
let resultArray; 
let unsafeResultArray; 
let sharedIntArraylSlice; 

global.onmessage = (message) => {};  
  1. onmessage 监听器中,获取 message 参数的数据组件,并使用 switchtype 属性上:
global.onmessage = (message) => { 
 const { data } = message; switch (data.type) {} 
};  
  1. 'load-shared-input' 添加一个案例,我们将数据对象的 input 属性分配给 sharedIntArray,在将其转换为 Int32Array 后:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
 case 'load-shared-input': sharedIntArray = new Int32Array(data.input); break; 
  } 
};  
  1. 'load-shared-output' 添加一个案例,我们将数据对象的 safeunsafe 属性分配给相应的结果数组,在将它们转换为 Int32Array 后:
 global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
 case 'load-shared-output': resultArray = new Int32Array(data.safe); unsafeResultArray = new Int32Array(data.unsafe); break; 
    } 
  }; 
  1. 'load-indices' 添加一个案例,根据当前索引和工作者总数计算当前工作者应该处理的价值范围:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
    case 'load-array': 
      sharedIntArray = new Int32Array(data.array); 
      break; 
 case 'load-indices': const { workerIndex, workerCount } = data; const windowSize = Math.floor(sharedIntArray.length / 
      workerCount); const startIndex = windowSize * workerIndex; const lastWorker = workerIndex === workerCount - 1; const endIndex = (lastWorker) ? sharedIntArray.length :
      startIndex + windowSize; sharedIntArraySlice = sharedIntArray.slice(startIndex,
      endIndex); break; } 
  }; 
  1. 'calculate-sum' 添加一个案例,该案例将数组中的所有数字相加,直接更新 unsafeResultArray,使用 Atomics.add 更新 resultArray,并将结果发送回主线程:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
   // ... 
 case 'calculate-sum': const sum = sharedIntArraySlice.reduce((acc, number) => acc + number , 0); sendResult('sum', sum); break; 
    } 
  }; 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

从结果中我们可以看出,unsafeResultArray 的值已经被竞争条件破坏。看起来好像有些值丢失了。然而,从工作者的角度来看,我们似乎将结果作为一个单一操作添加。

这并不完全正确。*+**=* 运算符实际上是三个单独的操作,一个读取操作、一个加法操作和一个写入操作。如果你想象多个工作者同时到达这个段(我们一次有 256 个在工作),那么你可以想象竞争条件是如何发生的。

原子操作防止这些错误发生。例如,Atomic.add 操作就像 +, = 是一个单一操作。当一个工作者使用 Atomics.add 或 API 中的任何其他方法时,他们可以确信,直到操作完成,值不会被另一个线程写入或读取。这就是为什么安全的总和总是与主线程上计算的总和相匹配,而不安全的总和可能会更少。

由于竞争条件是非确定性的,你可能需要多次运行这个菜谱,才能看到安全和不可安全总和之间的差异。

使用承诺为工作者提供一个简单的接口

到目前为止,我们已经看到了如何使用工作者(workers)来执行各种任务,但我们也已经看到它们的使用可能会很繁琐。这在某种程度上是不可避免的。然而,我们可以通过我们在前几章中已经看到的工具为使用工作者的操作提供良好的接口。

在这个菜谱中,我们将看到如何使用承诺(promises)来创建更熟悉的接口。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为 05-09-using-promise-for-simple-interfaces 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,其中包含一个名为 sumOnWorkerasync 函数:

// main.js
async function sumOnWorker(array) {}
  1. sumOnWorker 内部,返回一个新的承诺,其中你创建一个新的工作器,绑定 onmessage 事件监听器,并向工作器发送消息以计算总和:
// main.jsfunction sumOnWorker(array) { 
  return new Promise(function (resolve) { 
    const worker = new Worker('./worker.js'); 
 worker.onmessage = (message) => {}; worker.postMessage({ type: 'calculate-sum', array }); 
  }); 
} 
  1. onmessage 监听器内部,记录消息数据的 typeresult 属性,并解析 result
// main.js
async function sumOnWorker(array) { 
  return new Promise(function (resolve) { 
    const worker = new Worker('./worker.js'); 
    worker.onmessage = (message) => { 
 const { type, result } = message.data; console.log('Completed operation (%s), result: %s', type, result ); return resolve(result); 
    }; 

    worker.postMessage({ type: 'calculate-sum', array }); 
  }); 
} 
  1. 创建一个 async 的主函数,该函数创建三个随机数字的数组:
export function main() { 
  const array0 = (new Array(10000)).fill().map(Math.random); 
  const array1 = (new Array(1000)).fill().map(Math.random); 
  const array2 = (new Array(100)).fill().map(Math.random); 
} 
  1. 对每个数组调用 sumOnWorker,并记录结果:
export function main() { 
  // ... 
 sumOnWorker(array0).then((sum) => console.log('Array 0 sum: 
  %s', sum)); sumOnWorker(array1).then((sum) => console.log('Array 1 sum: 
  %s', sum)); sumOnWorker(array2).then((sum) => console.log('Array 2 sum: 
  %s', sum));; 
} 
  1. 创建一个 worker.js 文件,将当前上下文分配给变量 global,并将一个函数分配给 onmessage 事件:
// worker.js 
const global = this;  
global.onmessage = (message) => {};  
  1. onmessage 监听器中,获取 message 参数的数据组件,并根据 type 属性进行 switch
global.onmessage = (message) => { 
 const { data } = message; switch (data.type) {} 
};  
  1. 添加一个针对 'calculate-sum' 的用例,计算已发布的数组的总和。响应类型或操作,以及结果值:
global.onmessage = (message) => { 
  const { data } = message; 
  switch (data.type) { 
 case 'calculate-sum': const sum = data.array.reduce((acc, number) => acc + 
      number,0); global.postMessage({ type: 'sum', result: sum }); break;
  } 
}; 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

图片

它是如何工作的...

我们在前几章中看到了如何灵活地组合承诺和 async 函数。使用工作器这样做是一个自然的扩展。考虑一个异步 AJAX 请求。这可以被认为是在工作器中发生的。毕竟,它是在不同的执行线程中进行的,但由浏览器管理。

只要正确处理成功和错误条件,承诺和 async 函数就可以用来为 Web Workers 提供熟悉的接口。当将新技术与现有代码库集成时,拥有熟悉和简单的接口至关重要。

第六章:简单对象

在本章中,我们将涵盖以下配方:

  • 使用 Object.assign 向对象添加属性

  • 使用 Object.entries 获取可迭代的属性名对

  • 使用 Object.is 比较两个值

  • 在简单对象上定义函数属性作为方法

  • 使用 Object.defineProperty 定义只读属性

  • 使用 Object.defineProperty 覆盖只读属性

  • 使用 Object.defineProperty 创建非枚举属性

  • 使用对象结构创建对象

  • 使用解构从对象中选取值

  • 使用扩展运算符组合对象

简介

随着最近版本的 ECMAScript 中提供的所有新功能的推出,很容易忽视基础知识。对象 API,就像其他 API 一样,已经收到了很多更新。与不太熟悉的功能(如 SharedArrayBuffer)相比,它们可能看起来很平凡,但它们允许您创建一些有趣和有用的行为。

在本章中,我们将探讨如何使用对象 API 来创建丰富的关系和有趣属性。

使用 Object.assign 向对象添加属性

从不同的对象组合属性是一个相当常见的任务。这样做按值逐个进行是有限的且繁琐的,因为每个属性都必须枚举。这个配方演示了如何使用 Object.assign 方法完成相同的事情。

准备工作

这个配方假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做...

  1. 打开您的命令行应用程序,导航到您的 workspace。

  2. 创建一个名为 06-01-object-assign-add-properties 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 加载并运行一个 main 函数。

  4. 创建一个包含 main 函数的 main.js 文件,该函数创建两个对象,然后使用 Object.assign 将它们与另一个匿名对象组合:

// main.js 
export function main() { 
  const object = {}; 
  const otherObject = { 
    foo: 'original value', 
    bar: 'another value' 
  } 

  Object.assign(object, otherObject, { 
    foo: 'override value' 
  }); 

  console.log(object); 
}  
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您将看到以下输出:

它是如何工作的...

Object.assign 方法遍历传递给它的所有对象的属性。然后,它将这些属性分配给最左边的对象。优先考虑最右边对象的属性。因此,您可以看到 foo 的值来自匿名对象。最左边的对象被右边的值所突变,而其他对象保持不变。

我们将在稍后看到如何使用扩展运算符来完成相同的任务。

使用 Object.entries 获取可迭代的属性名对

Object.assign 在从一个对象复制属性到另一个对象方面表现良好。然而,我们有时想根据对象的属性执行其他操作。这个配方展示了如何使用 Object.entries 来获取一个对象属性的迭代器。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为06-02-object-entries-to-get-iterable的新文件夹。

  3. 复制或创建一个index.html文件,该文件加载并运行来自main.jsmain函数。

  4. 创建一个名为main的函数的main.js文件,然后创建一个对象,并使用for-of循环遍历Object.entries的结果:

// main.js 
export function main() { 
  const object = { 
    foo: Math.random(), 
    bar: Math.random() 
  }; 
  for (let [prop, value] of Object.entries(object)) { 
    console.log(prop, value); 
  } 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你将看到以下输出:

它是如何工作的...

Object.entries返回一个可迭代的值。这些列表可以用for-of循环遍历。这个可迭代中的项是属性名和值的对。属性是foobar,而值条目是这些属性的对应值。

语法[prop, value]将这个对拆分为单独的变量,然后输出。或者,我们也可以将条目propvalue作为数组的零和一索引来引用,但解构语法更直接。我们将在未来的食谱中查看解构。

使用Object.is比较两个值

JavaScript 与相等性有复杂的关系。众所周知,使用===比使用==更可取,因为它给出了更可预测的结果,在大多数情况下===的行为是预期的。不幸的是,由于 JavaScript 类型系统的怪癖,有一些令人沮丧的边缘情况。在本食谱中,我们将了解如何使用Object.is来获得比较的预期结果。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为06-03-compare-with-object-is的新文件夹。

  3. 复制或创建一个index.html文件,该文件加载并运行来自main.jsmain函数。

  4. 创建一个包含main函数的main.js文件,该函数进行一些说明性比较:

// main.js 
export function main() {
  const obj1 = {};
  const obj2 = {};

  console.log('obj1 === obj2', obj1 === obj2);
  console.log('obj1 is obj2', Object.is(obj1, obj2));
  console.log('obj2 === obj2', obj2 === obj2);
  console.log('obj2 is obj2', Object.is(obj2, obj2));
  console.log('undefined === undefined', undefined === 
  undefined);
  console.log('undefined is undefined', Object.is(undefined, 
  undefined));
  console.log('null === undefined', null === undefined);
  console.log('null is undefined', Object.is(null, undefined));

  // Special cases (from MDN documentation)
  console.log('Special Cases:');
  console.log('0 === -0', 0 === -0);
  console.log('0 is -0', Object.is(0, -0));
  console.log('-0 === -0', -0 === -0);
  console.log('-0 is -0', Object.is(-0, -0));
  console.log('NaN === NaN', NaN === NaN);
  console.log('NaN is NaN', Object.is(NaN, NaN));
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

它是如何工作的...

Object.is方法与===运算符有不同的规范。你可以看到它们大多数时候是一致的,但有一些边缘情况它们不一致。初始测试案例(objectnullundefined比较)都是一致的,但当我们遇到边缘情况时,我们开始看到一些差异。你可以看到正零和负零比较之间的差异,以及 NaN 比较。

更多信息,请参阅 Mozilla 开发者页面上的文档:

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is.

在普通对象上定义函数属性作为方法

在对象字面量上定义方法始终可以通过正常的键值对来实现。ECMAScript 的较新版本添加了一个简写,它模仿了在类上定义方法的语法。

在这个菜谱中,我们将看到我们可以使用这两种技术中的任何一种在对象字面量上创建和覆盖方法。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为06-04-define-function-properties-as-method的新文件夹。

  3. 复制或创建一个index.html文件,该文件从main.js加载并运行一个main函数。

  4. 创建一个名为main的函数的main.js文件,使用属性和方法语法定义两个方法,覆盖它们,并在覆盖前后调用它们:

// main.js  
export function main() { 
     const obj = { 
    method0: function() { 
      console.log('Hello, from method one.') 
    }, 
       method1() { 
      console.log('Hello, from method one.') 
    } 
  }; 
  obj.method0(); 
  obj.method1(); 

  obj.method0 = () => console.log('Override of method 0.'); 
  obj.method1 = () => console.log('Override of method 1.'); 
  obj.method0(); 
  obj.method1(); 
}   
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该会看到以下输出显示:

它是如何工作的...

正如我们之前看到的,两种语法是等效的。这两种方法被定义为具有函数值的属性,并且由于它们没有任何阻止其被覆盖的属性,因此可以被覆盖。method0method1都是在初始对象上定义的,并在之后立即执行。

接下来,我们为同一对象的这些属性分配一个新的函数值。因此,当它们再次被调用时,将执行新函数而不是原始函数。

在未来的菜谱中,我们将看到如何防止这种覆盖。

使用 Object.defineProperty 定义只读属性

并非总是理想的做法拥有可以被覆盖的方法。默认情况下,分配给对象的属性可以被重新分配。我们需要另一个选项来向对象添加函数,这样它们就不会被更改。

在这个菜谱中,我们将看到如何使用Object.defineProperty向对象添加不可写属性。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为06-05-define-readonly-props的新文件夹。

  3. 复制或创建一个index.html文件,该文件从main.js加载并运行一个main函数。

  4. 创建一个包含main函数的main.js文件,该函数定义一个不可写属性,然后尝试写入它:

 export function main() { 
  const obj = {}; 

  Object.defineProperty(obj, 'method1',{ 
    writable: false, 
    value: () => { 
      console.log('Hello, from method one.') 
    } 
  }); 
  obj.method1(); 

  // throws error 
  obj.method1 = () => console.log('Override of method 1.'); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该会看到以下输出显示:

它是如何工作的...

在这个菜谱中,我们看到对象属性不仅仅是简单的键值对。它们有属性来控制其行为。其中一个属性是可写的。这个属性意味着我们可以在之后重新赋值。默认情况下,这个属性被设置为 true;使用 Object.defineProperty 我们可以看到其他值。

我们将了解其他属性如何用来控制其他行为。

使用 Object.defineProperty 覆盖只读属性

创建不可写的属性并不是最终结果。在某些情况下,仍然有可能重写这些属性。幸运的是,这不太可能是一个意外行为。在这个菜谱中,我们将了解如何使用 Object.define 定义和重新定义不可写的属性。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 06-06-redefine-read-only-props 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个包含 main 函数的 main.js 文件,该函数创建一个对象。定义一个可配置的、不可写的属性 prop1,并赋予一个随机值:

export function main() { 
  const obj = {}; 

  Object.defineProperty(obj, 'prop1',{ 
    writable: false, 
    configurable: true, 
    value: Math.random() 
  }); 
  console.log(obj.prop1) 
} 
  1. 将该属性重新定义为另一个 random 值,并将可配置性更改为 false
export function main() { 
  // ... 
 Object.defineProperty(obj, 'prop1',{ writable: false, configurable: false, value: Math.random() }); console.log(obj.prop1)}
  1. 尝试第三次重新定义属性:
 export function main() { 
  // ... 
 // throws error Object.defineProperty(obj, 'prop1',{ value: Math.random() }); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 你应该会看到以下输出显示:

图片

工作原理...

可写性只是方程的一部分。将可写性设置为 false 意味着属性可以被正常重新赋值。默认情况下,它不能使用 Object.defineProperty 重新定义。然而,如果 configurable 设置为 true,则可以重新定义属性。一旦将 configurable 属性设置为 false,我们就不能再重新定义它了。

使用 Object.defineProperty 创建不可枚举的属性

在之前的菜谱中,我们看到了如何避免属性被覆盖。有些情况下,我们可能不希望属性被读取。回想一下 Object.entries 方法,它创建了一个包含对象上所有属性和值的迭代器。嗯,这并不完全正确。它创建了一个包含所有 enumerable 属性的迭代器。

在这个菜谱中,我们将了解如何创建不会被包含在迭代器中的属性。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 06-07-non-enumerable-props 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,其中包含 main 函数,该函数创建一个包含书籍和作者键值对的对象:

// main.js
export function main() { 
  const bookAuthors = { 
    "Star's End": "Cassandra Rose Clarke", 
    "Three Body Problem": "Liu Cixin", 
    "Houston Houston, Do You Read?": "James Tiptree Jr." 
  };  
} 
  1. 定义两个属性,一个是有随机值的 enumerable,另一个是非可枚举的,其值为函数:
export function main() { 
  // ... 
 Object.defineProperty(bookAuthors, 'visibleProp', { enumerable: true, value: Math.random() }); Object.defineProperty(bookAuthors, 'invisibleProp', { value: () => console.log('This function is hidden.') }); for (const [prop, value] of Object.entries(bookAuthors)) { console.log(prop, value) } bookAuthors.invisibleProp();
 } 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

图片

它是如何工作的...

在对象上以字面量定义的键值与之前一样是可枚举的。接下来的两个属性更有趣。第一个属性 visibleProp 明确将 enumerable 属性设置为 true,并出现在列表中。invisibleProp 属性没有明确设置;默认值为 false。只有可枚举的属性出现在迭代器中。

使用对象结构创建对象

从对象中提取属性是另一个重复的任务。似乎存在不必要的重复。ECMAScript 的新版本包括一种语法特性,使这个过程不那么繁琐。这个配方演示了如何使用对象解构从对象属性中提取新变量。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 06-08-pick-values-from-object-destructuring 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 中加载并运行一个 main 函数。

  4. 创建一个 main.js 文件。创建一个主函数,该函数创建一个新对象,然后从其中的属性创建新的常量:

// main.js
export function main() { 
  const object = { 
    prop1: 'some value', 
    prop2: 'some other value', 
    objectProp: { foo: 'bar' } 
  }; 

  const { prop1, prop2, objectProp } = object; 
  console.log(prop1); 
  console.log(prop2); 
  console.log(objectProp); 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

图片

它是如何工作的...

解构是一种语法简写。变量的名称用于引用对象上的属性。值被分配给对应名称的变量。

使用解构从对象中提取值

将多个属性捆绑成一个单一的对象是 JavaScript 中的另一个常见任务,这可能会非常繁琐。ECMAScript 的较新版本增加了一种新的语法,使这个过程更加方便。

在这个配方中,我们将看到如何使用这种新语法从现有变量中创建一个对象。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 06-09-create-objects-with-structuring 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 中加载并运行一个 main 函数。

  4. 创建一个名为 mainasync 函数的 main.js 文件,该函数创建几个常量,然后使用对象结构创建具有相应名称和值的对象:

// main.js
export function main() { 
  const prop1 = 'some value'; 
  const prop2 = 'some other value'; 
  const objectProp = { foo: 'bar' }; 
  const object = { prop1, prop2, objectProp }; 

  console.log(object); 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

它是如何工作的...

与之前的菜谱一样,语法使用花括号之间的变量名来确定对象上的属性名。每个属性都使用相应的常量的变量名和值创建。

使用扩展操作符合并对象

在之前的菜谱中,我们看到了如何使用 Object.assign 来合并对象。它完成了任务,但通过使用更新的 ECMAScript 语法,我们可以以更紧凑的方式完成这项任务。在这个菜谱中,我们将看到如何使用新的扩展操作符来合并对象。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 06-10-spread-operator-combine 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,其中包含一个名为 mainasync 函数,该函数创建几个对象和一个常量。然后它使用扩展操作符和对象结构将它们合并成一个单一的对象:

// main.js
export function main() { 
  const object1 = { 
    prop1: 'some value', 
    prop2: 'some other value', 
  } 
  const object2 = { 
    prop2: 'some overriding value', 
    objectProp: { foo: 'bar' } 
  } 
  const anotherProp = Math.random(); 

  const combinedObject = { ...object1, ...object2, anotherProp }; 
  console.log(combinedObject); 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

它是如何工作的...

扩展操作符将对象的 enumerable 属性展开,以便在构建新对象时它们都被引用。像 Object.assign 一样,值从右到左给予优先级,并且最后一个属性的处理方式与之前菜谱中对象结构的方式相同。

第七章:创建类

在本章中,我们将涵盖以下食谱:

  • 创建一个新类

  • 使用构造函数参数分配属性

  • 在类上定义方法

  • 使用 instanceOf 检查实例类型

  • 使用获取器创建只读属性

  • 使用设置器封装值

  • 使用静态方法处理所有实例

简介

使用 JavaScript 的原型继承模型,创建和扩展类似的对象一直是可能的。通过使用 new 操作符和添加原型属性,我们可以创建类似类的结构。

ECMAScript 2015 引入了类语法,作为一种更友好的方式来处理原型继承。有人认为这种 语法糖 不值得拥有两种实现 OOP 结构的方法所带来的开销。然而,我认为类提供了一种更简洁地表达相同想法的方法,并且是一个净收益。正如我们将在本章和下一章中看到的,类语法使得表达复杂的 OOP 关系变得更加容易。

创建一个新类

最基本的任务类可以用作创建一个新的类。本食谱展示了定义和实例化新类的简单语法。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 07-01-create-a-new-class 的新文件夹。

  3. 复制或创建一个 index.html,它从 main.js 加载并运行一个 main 函数。

  4. 创建一个 main.js 文件,定义一个名为 Rocket 的新类和一个 main 函数,该函数创建两个实例并将它们输出:

// main.js 
class Rocket {} 

export function main() { 
  const saturnV = new Rocket(); 
  const falconHeavy = new Rocket(); 
  console.log(saturnV); 
  console.log(falconHeavy); 
}  
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您将看到以下输出:

图片

它是如何工作的...

classRocket {} 语法相当于创建一个名为 Rocket 的函数(注意已记录对象的 constructor 值)。这意味着在 JavaScript 中,可以使用 new 操作符创建 Rocket 的实例。这基于原型创建了一个对象。

我们将在未来的食谱中看到如何创建一些更有趣的对象。

使用构造函数参数分配属性

现在我们有一个新的类,是时候开始区分实例了。在本食谱中,我们将看到如何在创建实例时通过构造函数参数分配属性。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 07-02-assigning-constructor-props 的新文件夹。

  3. 复制或创建一个 index.html,它从 main.js 加载并运行一个 main 函数。

  4. 创建一个main.js文件,创建一个名为Rocket的新类。添加一个constructor方法,该方法接受一个单一参数name,并将其分配给方法体内的同名属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 创建一个main函数,该函数创建两个实例并使用它们的属性记录它们:
// main.js 
export function main() { 
  const saturnV = new Rocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 
  console.log(saturnV.name, saturnV); 
  console.log(falconHeavy.name, falconHeavy); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

它是如何工作的...

正如我们在前面的配方中看到的,类语法如果未定义,将创建一个空的constructor函数。constructor是一个在类实例化后立即调用的方法。在这里,我们按照以下方式创建一个新的Rocket实例:

const saturnV = new Rocket('Saturn V');

这意味着名称属性实际上是在从new表达式返回并分配给saturnVRocket实例之前设置的。

在这个配方中,我们定义了constructorconstructor函数的上下文,即this的值,是新对象的实例。因此,当我们为thisname属性赋值时,它是在那个新实例上设置的。

在类上定义方法

保存值的类并不特别有趣。我们还想让它们能够有一些行为,这些行为作为对外部世界的接口。在这个配方中,我们将看到如何向类添加方法。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为07-03-defining-methods的新文件夹。

  3. 复制或创建一个index.html,该文件从main.js加载并运行一个main函数。

  4. 创建一个包含名为Rocket的类的main.js,在构造函数中分配一个name属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 添加一个名为takeoff的方法,该方法接受一个选项倒计时参数。方法体应该在超时前后记录一条消息:
// main.js 
class Rocket { 
  // ... 
  takeOff(countdown = 1000) { 
    console.log(this.name + ' starting countdown.'); 
    setTimeout(() => { 
      console.log(`Blastoff! ${this.name} has taken off`); 
    }, countdown); 
  } 
} 
  1. 添加一个main函数,该函数创建两个实例然后调用它们的takeOff方法:
// main.js 
export function main() { 
  const saturnV = new Rocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 
  saturnV.takeOff(500); 
  falconHeavy.takeOff(); 
}  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你将看到以下输出:

它是如何工作的...

在类定义体中添加方法与将属性附加到函数的原型相同,其值是函数。这意味着这些属性作为属性添加到新对象的实例中。当调用这些方法时,上下文(this的值)是当前实例。

使用 instanceOf 检查实例类型

在许多情况下,例如参数验证,我们希望检查对象的类。因为 JavaScript 不是静态类型,我们无法在程序开始之前保证函数接收正确的类型参数,我们需要在运行时进行检查。

在本食谱中,我们将了解如何使用instanceOf运算符在运行时检查一个对象的原型。

准备工作

本食谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做...

  1. 打开您的命令行应用程序,导航到您的开发空间。

  2. 创建一个名为07-04-checking-with-instanceof的新文件夹。

  3. 复制或创建一个index.html,该文件加载并运行来自main.jsmain函数。

  4. 创建一个包含两个相同类RocketInactiveRocketmain.js

// main.js class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 

class InactiveRocket { 
  constructor(name) { 
    this.name = name; 
  } 
}  
  1. 创建一个名为printRocketType的函数,该函数使用instanceOf来区分火箭类:
// main.js class Rocket { 
function printRocketType(rocket) { 
  if (rocket instanceof InactiveRocket) { 
    console.log(rocket.name + ' is an inactive rocket'); 
  } else { 
    console.log(rocket.name + ' is active'); 
  } 
} 
  1. 创建一个main函数,该函数创建任一类的火箭实例,然后对它们都调用printRocketType
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  [saturnV, falconHeavy].forEach(printRocketType); 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出显示:

图片

它是如何工作的...

instanceOf运算符比较左侧值的原型与右侧值。如果两者匹配,表达式将被评估为真,否则评估为假。因此,我们可以在条件表达式中使用它。

使用 getter 创建只读属性

我们并不总是希望属性可写。在之前的食谱中,我们看到了如何在对象上创建只读属性。在本食谱中,我们将了解如何在类体中使用get关键字来完成此操作。

准备工作

本食谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做...

  1. 打开您的命令行应用程序,导航到您的开发空间。

  2. 创建一个名为07-05-getters-read-only的新文件夹。

  3. 复制或创建一个加载并运行来自main.jsmain函数的index.html

  4. 创建一个包含Rocket类并定义只读属性的main.js文件:

class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 

  get readOnlyValue() { 
    return 'Cant' touch this.'; 
  } 
} 
  1. 创建一个名为main的函数,该函数创建Rocket类的实例。从可写和只读属性中读取,然后尝试写入它们:
export function main() { 
  const saturnV = new Rocket('Saturn V'); 

  console.log(saturnV.name); 
  saturnV.name = 'Saturn Five'; 
  console.log(saturnV.name); 

  console.log(saturnV.readOnlyValue); 
  // throws error 
  saturnV.readOnlyValue = 'somethingElse'; 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出显示:

图片

它是如何工作的...

使用get关键字创建一个具有函数值的只读属性。我们从该函数返回一个字符串,因此,当读取属性时,返回该值。然而,因为它只读,所以当我们尝试写入它时,会抛出一个运行时错误。

使用 setter 来封装值

在上一个食谱中,我们看到了如何防止值被写入然而,有时我们不想阻止一个属性被写入。相反,我们希望控制其写入方式。在本食谱中,我们将了解如何使用set来控制属性的写入。

准备工作

此配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为07-06-setters-encapsulate的新文件夹。

  3. 复制或创建一个index.html,该文件加载并运行来自main.jsmain函数。

  4. 创建一个包含Rocket类并在构造时写入_secretName属性的main.js文件:

class Rocket { 
  constructor(name) { 
    this._secretName = name; 
  }  
} 
  1. name属性添加一个获取器和设置器,并且只有当newValue是一个字符串时才更新它:
class Rocket { 
  // ... 
 get name() { return this._secretName;
 } set name(newValue) { if (typeof newValue === 'string') { this._secretName = newValue; } else { console.error('Invalid name: ' + newValue) } }
  1. 创建一个尝试将name属性设置为不同值的main函数:
export function main() { 
  const saturnV = new Rocket('Saturn V'); 
  console.log(saturnV.name) 

  saturnV.name = 'Saturn Five'; 
  console.log(saturnV.name) 

  saturnV.name = 5; 
  console.log(saturnV.name) 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

图片

它是如何工作的...

get关键字一样,set关键字在写入属性时调用一个函数属性。我们不是直接设置值,而是能够执行类型检查。如果newValue是一个字符串,它将按正常方式写入。否则,我们记录一个错误,并且不设置秘密属性的值。

显然,_secretName可以直接编写。这必须通过文档来解决。库的用户应该只使用公共接口。他们自行承担风险!

使用静态方法处理所有实例

将方法组织在类上,而不是类的实例上可能是一个好主意。一个例子是管理器模式。当对象创建成本高昂或将被大量重用时,此模式非常有用。

在此配方中,我们将看到如何使用static关键字为Rocket类的实例创建一个映射。

准备工作

此配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为07-07-static-methods-on-all-instances的新文件夹。

  3. 复制或创建一个index.html,该文件加载并运行来自main.jsmain函数。

  4. 创建一个包含空对象rocketMap和类Rocketmain.js文件:

//main.js
let rocketMap = {};  

class Rocket {} 
  1. 创建一个名为find的静态方法,该方法通过字符串在Rocket类中查找火箭:
class Rocket { 
  // ... 
 static find (name) { return rocketMap[name]; } 
} 
  1. 添加一个分配名称属性的构造函数,并将实例分配给rocketMap
class Rocket { 
  // ... 
 constructor(name) { this.name = name; rocketMap[name] = this; } 
} 
  1. 创建一个比较Rocket类创建的实例与静态find方法结果的main函数:
//main.js
export function main() { 
  const saturnV = new Rocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  console.log('Is Saturn V?', saturnV === Rocket.find('Saturn 
  V')); 
  console.log('Is Falcon Heavy?', falconHeavy === 
  Rocket.find('Saturn V')); 
  console.log('Is Same Rocket?', Rocket.find('Saturn V') === 
  Rocket.find('Saturn V')); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出显示:

图片

它是如何工作的...

静态关键字意味着该函数将成为Rocket类的属性,而不是一个实例。这与直接在函数上设置函数属性,而不是在其原型上设置相同。因此,我们将该函数称为类的属性,而不是实例的属性。

第八章:继承和组合

本章将涵盖以下内容:

  • 扩展一个类

  • 使用构造函数参数分配额外的属性

  • 覆盖父类实例方法

  • 覆盖父类静态方法

  • 调用超类方法

  • 使用组合而不是继承来添加行为

  • 使用混入(mix-ins)来添加行为

  • 将一个类作为参数传递

  • 使用 Object.getPrototypeOf 检查类继承

  • 使用 throw 来模拟抽象类

简介

在上一章中,我们看到了如何使用新的类语法来实现仅比直接使用原型实现稍微复杂的行为。开发者可能会认为这种语言复杂度不值得,毕竟,仅多一行(或可能是一个字符)就能得到相同的行为。

使用新的 ES6 类语法创建对象原型的真正优势在于使用更复杂结构和技术的过程中显现出来。本质上,我们将看到,当行为用关键字定义时,代码更容易理解,而不是用上下文敏感的操作符。

在本章中,我们将探讨如何使用类实现一些更复杂的行为。

扩展一个类

扩展类可以用来允许新的行为,同时遵循通用接口。虽然这并不是组织对象之间关系总是最好的方式,但在许多情况下,扩展(有时称为继承)是构建行为最有效的方法。

在本例中,我们将看到一个非常简单的扩展示例。

准备工作

本例假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做到这一点...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 08-01-extending-classes 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 加载并运行一个 main 函数。

  4. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 InactiveRocket 的类,它扩展了 Rocket 类:
// main.js 
class InactiveRocket extends Rocket {} 
  1. 创建一个 main 函数,创建两个类的实例并输出它们的名称:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  console.log(saturnV.name, ' is a rocket.'); 
  console.log(falconHeavy.name, ' is also a rocket.'); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您将看到以下输出:

图片

它是如何工作的...

classInactiveRocket 扩展了 Rocket。这意味着除非被覆盖,否则 Rocket 原型上的所有属性最终都会出现在 InactiveRocket 的实例上。构造函数是特殊的,但也是 Rocket 原型上的一个属性。因此,当创建两个实例时,Rocket 类的构造函数会为 RocketInactiveRocket 实例执行。所以,我们看到两个实例上都分配了 name 属性。

我们将在未来的食谱中了解如何覆盖方法和其他行为。

使用构造函数参数分配额外的属性

如果我们正在扩展一个类,我们希望它有一些不同。否则,扩展它的意义何在?在本食谱中,我们将通过添加额外的属性来区分子类。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 08-02-additional-constructor-args 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 InactiveRocket 的类,该类扩展了 Rocket 类,并在构造函数中分配一个额外的 lastFlown 属性:
// main.js 
class InactiveRocket extends Rocket { 
  constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
  } 
} 
  1. 创建一个 main 函数,该函数创建两个类的实例并输出它们的名称,以及 InactiveRocketlastFlown 属性:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14,1973')); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  console.log(falconHeavy.name + ' is a Rocket'); 
  console.log(saturnV.name + ' is an inactive rocket'); 
  console.log(`${saturnV.name} was last flown:
  ${rocket.lastFlown}`); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

工作原理...

本食谱与前面的简单扩展示例不同。通过在 InactiveRocket 上实现构造函数,我们能够传递一组不同的参数。lastFlown 属性是 InactiveRocket 独有的。因此,我们在 InactiveRocket 的实例上看到该属性,但在 Rocket 的实例上没有看到。

您会注意到在 InactiveRocket 的构造函数中调用了 super 方法。这手动执行了当前实例的 Rocket 构造函数。这就是为什么 name 属性也被附加的原因。如果我们没有执行 super,那么 Rocket 构造函数就不会被调用。

这样,我们保留了父类 Rocket 的属性,并为 InactiveRocket 子类添加了一个额外的属性。

覆盖父类实例方法

理想情况下,类不仅包含属性,还定义行为。因此,子类也应该扩展行为,而不仅仅是添加额外的属性。

在本食谱中,我们将了解如何覆盖父类的方法。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 08-03-defining-methods 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,定义一个名为 Rocket 的新类 添加一个构造函数,该构造函数接受一个构造函数参数 name 并将其分配给实例属性。然后,定义一个简单的 print 方法:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 

  print() { 
    console.log(this.name + ' is a Rocket'); 
  } 
}  
  1. 创建一个名为InactiveRocket的类,它扩展了Rocket类,并在构造函数中分配一个额外的lastFlow属性。然后,覆盖print方法以包含新属性:
// main.js 
class InactiveRocket extends Rocket { 
  constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
  } 

  print() { 
    console.log(this.name + ' is an inactive rocket'); 
    console.log(`${this.name} was last flown: 
    ${this.lastFlown}`); 
  } 
} 
  1. 创建一个main函数,创建两个类的实例,并调用两个类的print方法:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14,1973')); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  [saturnV, falconHeavy].forEach((r) => r.print()); 
}  
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

在类定义体中添加方法与将属性附加到函数的原型相同,其中函数是它们的值。这意味着这些属性被添加为新对象的实例属性。当调用这些方法时,上下文(this的值)是当前实例。

这与第六章中“在普通对象上定义函数属性作为方法”的菜谱类似,普通对象。在那个菜谱中,我们通过直接赋值来覆盖方法。相比之下,在这个菜谱中,我们在原型上这样做。这意味着这个覆盖适用于InactiveRocket子类的每个实例。

覆盖父类静态方法

我们之前已经看到,行为不仅限于类实例,也附加到类本身。这些静态方法也可以被子类覆盖。

在这个菜谱中,我们将看到如何覆盖静态方法。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为08-04-checking-with-instanceof的新文件夹。

  3. 复制或创建一个index.html,它从main.js加载并运行一个main函数。

  4. 创建两个对象rocketMapinactiveRocketMap

// main.js 
let rocketMap = {};  
let inactiveRocketMap = {}; 
  1. 定义一个名为Rocket的新类。添加一个构造函数。使用名称将实例分配给rocketMap,并定义一个简单的print方法:
// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
    rocketMap[name] = this; 
     } 
  print() { 
    console.log(this.name + ' is a rocket'); 
  } 
}  
  1. 添加一个静态find方法,从rocketMap检索实例:
// main.js 
class Rocket { 
  static find (name) { 
    return rocketMap[name]; 
  } 
} 
  1. 创建一个名为InactiveRocket的类,它扩展了Rocket类,并在构造函数中分配一个额外的lastFlow属性。使用name将实例分配给inactiveRocketMap,并覆盖print方法以包含新属性:
// main.js 
class InactiveRocket extends Rocket { 
 constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
    inactiveRocketMap[name] = this; 
  } 

  print() { 
    console.log(this.name + ' is an inactive rocket'); 
    console.log(`${this.name} was last flown: 
    ${this.lastFlown}`); 
  } 
} 
  1. 添加一个静态find方法,从rocketMap检索实例:
// main.js 
class InactiveRocket { 
  static find (name) { 
    return inactiveRocketMap[name]; 
  } 
} 
  1. 创建一个main函数,创建两个类的实例,并尝试从映射中检索实例:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V'); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  // print rocket for saturn V and falcon heavy 
  console.log('All Rockets:'); 
  Rocket.find('Saturn V').print(); 
  Rocket.find('Falcon Heavy').print(); 

  // print inactive entry for saturn v and attempt falcon 
  console.log('Inactive Rockets:'); 
  InactiveRocket.find('Saturn V').print(); 
  // throws an error 
  InactiveRocket.find('Falcon Heavy').print(); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

就像实例方法一样,在子类上定义的静态方法会覆盖父类上的方法。我们创建了一个带有静态方法的Rocket类,该方法根据其名称定位Rocket实例。我们还创建了一个具有自己的find方法的InactiveRocket类,该方法在另一个映射中搜索。因为InactiveRocket类在其构造函数中调用super,所以两个类的实例都被添加到Rocketfind方法使用的映射中。而只有InactiveRocket类的实例被添加到该类find方法使用的映射中。

当我们在Rocket类上调用find时,我们能够检索到两个类的实例。你会注意到,我们无法使用InactiveRocket类的find方法定位base类的实例。

调用 super 方法

重写方法是扩展行为的好方法。然而,我们有时还想继续使用父类中的行为。通过使用super关键字来访问父类方法,这是可能的。

在这个食谱中,我们将看到如何使用这个关键字来访问这些方法。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为08-05-getters-read-only的新文件夹。

  3. 复制或创建一个index.html文件,该文件加载并运行来自main.jsmain函数。

  4. 创建一个main.js文件,定义一个名为Rocket的新类。添加一个接受构造函数参数name并将其分配给实例属性的构造函数。然后,定义一个简单的print方法:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 

  print() { 
    console.log(this.name + ' is a Rocket'); 
  } 
} 
  1. 创建一个名为InactiveRocket的类,它扩展了Rocket类并在构造函数中分配一个额外的lastFlow属性。然后,重写print方法并调用super.print
// main.js 
class InactiveRocket extends Rocket { 
  constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
  } 

  print() { 
    super.print(); 
    console.log(`${this.name} was last flown: 
    ${this.lastFlown}`); 
  } 
} 
  1. 创建一个main函数,创建两个类的实例并调用它们的print方法:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14, 1973')); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  falconHeavy.print(); 
  saturnV.print(); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

因为我们在InactiveRocket类中重写了print方法,调用该方法将执行该类中定义的代码,而不是父类。super关键字允许我们引用父类的原型。所以,当在super关键字上调用print方法时,将执行在父类原型上定义的方法。因此,我们可以看到saturnV实例的print方法的输出。

使用组合而不是继承来添加行为

到目前为止,我们已经看到了如何使用继承来添加行为和组合更大的结构。这并不总是理想的方法。在许多情况下,使用一种称为组合的方法会更好。这涉及到使用没有建立层次关系的不同类进行连接。这里的主要优势是代码清晰度和灵活性。

在这个菜谱中,我们将看到如何使用组合。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何操作...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 08-06-using-composition-instead-of-inherritence 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,定义一个名为 Rocket 的新类。添加一个接受构造函数参数 name 并将其分配给实例属性的构造函数。然后,定义一个简单的 print 方法:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 

  print() { 
    console.log(this.name + ' is a Rocket'); 
  } 
}  
  1. 创建一个名为 InactiveRocket 的类,它扩展了 Rocket 类并在构造函数中分配一个额外的 lastFlow 属性。然后,重写 print 方法:
// main.js 
class InactiveRocket extends Rocket { 
 constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
  } 

  print() { 
    console.log(this.name + ' is an inactive rocket'); 
    console.log(`${this.name} was last flown: 
    ${this.lastFlown}`); 
  } 
} 
  1. 创建一个名为 Launcher 的类,它接受一个构造函数参数 rocket。添加一个名为 prepareForLaunch 的方法,如果火箭不活跃则中止:
// main.js 
class Launcher { 
  constructor (rocket) { 
    this.rocket = rocket; 
  } 

  prepareForLaunch () { 
    const { rocket } = this; 

    if (rocket instanceof InactiveRocket) { 
      console.error(`Unable to launch, rocket ${rocket.name} has 
      been inactive since ${rocket.lastFlown}`); 
    } else { 
      console.log(`${rocket.name} is ready to launch.`); 
    } 
  } 
}  
  1. 创建一个 main 函数,创建两个 Launcher 实例;每个火箭类一个:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14,1973')); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  const saturnVLauncher = new Launcher(saturnV); 
  const falconHeavyLauncher = new Launcher(falconHeavy); 

  saturnVLauncher.prepareForLaunch(); 
  falconHeavyLauncher.prepareForLaunch(); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

在这个菜谱中,我们添加了一个类的实例(两个 Rocket 类中的一个)并在另一个类的实例(Launcher)中使用它,这两个类之间没有通过继承相关联。通过一个 LaunchableRocket 类添加这种发射功能是可能的,但随着火箭类型及其关系的变化,这会变得繁琐。

通常,通过继承添加行为是受限的;它应该限制在小的更改上,并且它们不应该在公共接口上有所不同。依赖倒置原则(Dependency Inversion Principle)DIP)在考虑继承时是一个重要的概念。

访问以下链接了解 DIP(依赖倒置原则)的更多信息:en.wikipedia.org/wiki/Dependency_inversion_principle.

使用混入添加行为

我们已经看到了如何使用继承和组合来添加行为。还有一种不同的组合方法,它在不使用继承的情况下将行为附加到现有类上。使用混入(mix-ins)可以在运行时将属性附加到对象实例。

在这个菜谱中,我们将看到如何使用混入(mix-ins)在不使用继承的情况下向类添加共享行为。

准备工作

此配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何实现...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 08-08-using-mixins 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,该文件定义了一个名为 Rocket 的新类。在构造函数中,使用名为 Launcher 的对象扩展当前实例:

// main.js 
class Rocket { 
  constructor(name) { 
    Object.assign(this, Launcher); 
    this.name = name; 
  } 

  print() { 
    console.log(this.name + ' is a rocket'); 
  } 
}  
  1. 创建一个名为 InactiveRocket 的类,该类继承自 Rocket 类,并在构造函数中分配一个额外的 lastFlow 属性:
// main.js 
class InactiveRocket extends Rocket { 
 constructor(name, lastFlown) { 
    super(name); 
    this.lastFlown = lastFlown; 
  } 

  print() { 
    console.log(this.name + ' is an inactive rocket'); 
    console.log(`${this.name} was last flown: 
    ${this.lastFlown}`); 
  } 
} 
  1. 创建一个名为 Launcher 的对象,该对象定义了一个名为 prepareForLaunch 的方法,如果火箭处于非活动状态,则该方法将终止:
// main.js 
const Launcher = { 
  prepareForLaunch () { 
    if (this instanceof InactiveRocket) { 
      console.error(`Unable to launch, rocket ${this.name} has 
      been inactive since ${this.lastFlown}`); 
    } else { 
      console.log(`${this.name} is ready to launch.`); 
    } 
  } 
}  
  1. 创建一个名为 main 的函数,该函数创建 Rocket 类的每个实例,并对每个实例调用 prepareForLaunch 方法:
// main.js 

export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14,1973')); 
  const falconHeavy = new Rocket('Falcon Heavy'); 

  saturnV.prepareForLaunch(); 
  falconHeavy.prepareForLaunch(); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

Object.assign 将一个对象的所有属性添加到另一个对象中。将 Launcher 的所有属性分配给新实例,使得在对象创建后这些属性可用。记住,this 上的方法只是具有函数值的原型上的属性。因此,以这种方式添加方法是定义这些方法在原型上的等效。

因此,在应用 Object.assign 混合后,我们可以调用在 Launcher 上定义的作为 RocketInactiveRocket 的实例方法的属性。

传递一个类作为参数

类,就像函数一样,是 JavaScript 的一等公民。这意味着它们可以从函数中返回或作为参数传递。在这个配方中,我们将看到如何使用后者。

准备就绪

此配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何实现...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 08-08-passing-class-as-an-argument 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个名为 main.js 的文件,该文件定义了一个名为 Rocket 的新类:

// main.js 
class Rocket {
  constructor(name) { 
    this.name = name; 
  }
}
  1. 创建一个名为 InactiveRocket 的类,该类继承自 Rocket 类,并在构造函数中分配一个 name 和一个 lastFlow 属性:
// main.js 
class InactiveRocket extends Rocket { 
 constructor(name, lastFlown) { 
    super(); 
    this.lastFlown = lastFlown; 
 } 
} 
  1. 创建一个名为 isA 的函数,该函数接受一个实例和一个 klass 参数,如果构造函数是传递的类,则返回 true
// main.js 
function isA(instance, klass) { 
  return instance.constructor === klass; 
} 
  1. 创建一个名为 main 的函数,该函数创建 InactiveRocket 的一个实例。调用 isA 方法来比较实例与两个 Rocket 类:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V', new Date('May 
  14,1973')); 

  console.log(saturnV.name + ' instance of Rocket: ' + 
  isA(saturnV,Rocket)); 
  console.log(saturnV.name + ' instance of InactiveRocket: ' + 
  isA(saturnV, InactiveRocket)); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

isA函数比较instance构造函数属性与传递的klass的标识。如果instance参数是klass的实例(在这种情况下,我们正在比较InactiveRocket类的实例),则返回true。对于任何其他类,包括Rocket,它将返回false

因为我们是直接将构造函数与类进行比较,所以不考虑继承。如果我们使用instanceOf,该函数将返回true对于Rocket

使用Object.getPrototypeOf检查类继承

我们已经看到了如何通过布尔表达式检查实例化类的继承和身份。我们可能还想一次性查看实例的完整继承关系。在这个菜谱中,我们将看到如何做到这一点。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何实现...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为08-09-checking-class-inheritance的新文件夹。

  3. 复制或创建一个index.html,该文件加载并运行来自main.jsmain函数。

  4. 创建一个定义三个新Rocket类的main.js文件:

// main.js 
class Rocket {} 
class ActiveRocket extends Rocket {} 
class OrbitingRocket extends ActiveRocket {}  
  1. 创建一个名为listInheritance的函数,它接受一个实例并使用Object.getPrototypeOf获取所有类直到达到 null 类型:
// main.js 
function listInheritance (instance) { 
  const hierarchy = []; 
  let currClass = instance.constructor; 

  while (currClass.name) { 
    hierarchy.push(currClass.name); 
    currClass = Object.getPrototypeOf(currClass) 
  } 

  console.log(hierarchy.join(' -> ')); 
} 
  1. 创建一个main函数,该函数创建一个OrbitingRocket实例并列出其继承关系:
// main.js 
export function main() { 
  const orbitingRocket = new OrbitingRocket(); 
  listInheritance(orbitingRocket); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

所有 JavaScript 原型都存在于一个层次结构中。这意味着每个原型都从一个另一个扩展而来,反过来又从另一个扩展而来。在层次结构的顶部是 null 类型。Object.getPrototypeOf沿着继承树向上遍历,从实例的原型一直爬到 null 类型。然后我们可以使用每个的name属性,这将给出该原型的名称(或在我们的情况下是类)。

使用throw来模拟抽象类

到目前为止,我们已经看到了如何创建和组合类以形成各种不同的形状。然而,有时我们想要能够防止创建一个类,并且只允许扩展类的实例。其他语言提供了一个称为抽象类的功能。在这个菜谱中,我们将看到如何通过抛出错误来模拟这一点。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何实现...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为08-10-use-throw-to-simulate-abstract-class的新文件夹。

  3. 复制或创建一个index.html,该文件加载并运行来自main.jsmain函数。

  4. 创建一个main.js文件,该文件定义一个名为Rocket的新类。在构造函数中,检查实例的构造函数,如果是Rocket,则抛出错误:

// main.js 
class Rocket { 
  constructor (name) { 
    this.name = name; 
    if (this.constructor === Rocket) { 
      throw new Error('Abstract Class Should not be 
      instantiated'); 
    } 
  } 
}  
  1. 创建Rocket的两个子类:
// main.js 
class ActiveRocket extends Rocket {} 
class InactiveRocket extends Rocket {} 
  1. 创建一个main函数,该函数创建每个火箭类实例。注意,Rocket类不能被实例化:
// main.js 
export function main() { 
  const saturnV = new InactiveRocket('Saturn V'); 
  console.log(saturnV.name, ' is a Rocket ', saturnV instanceof 
  Rocket); 

  const falconHeavy = new ActiveRocket('Falcon Heavy'); 
  console.log(falconHeavy.name, ' is a Rocket ', falconHeavy 
  instanceof Rocket); 

  // throws an error; 
  new Rocket('Not going to make it!'); 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

图片

它是如何工作的...

这样做的目的是强制用户扩展Rocket类,而不是直接实例化它。然而,我们仍然希望能够在base类中定义行为。在Rocket类的构造函数中,你可以看到这是如何实现的。通过比较实例的构造函数属性与Rocket,我们可以看到这个类是否被直接实例化。如果类被扩展,那么这个比较将评估为false,错误将不会抛出。因此,我们可以创建ActiveRocketInactiveRocket实例。

当直接创建Rocket实例时,构造函数比较结果为true,错误被抛出。因此,我们无法创建Rocket类的实例,只能创建它的子类实例。

第九章:使用设计模式构建更大的结构

在本章中,我们将介绍以下食谱:

  • 使用模板函数定义步骤

  • 使用构建器组装自定义实例

  • 使用工厂复制实例

  • 使用访问者模式处理结构

  • 使用单例管理实例

  • 修改现有设计模式以适应不同的用例

  • 将现有设计模式组合起来以适应不同的用例

简介

类,就像对象和函数一样,是我们创建程序的基本构建块。随着程序的扩展,定义实体之间关系变得越来越困难,且需要高效和系统地定义。当数据与功能之间的关系变得复杂时,我们可以使用类和其他对象来组织它们。但是,当类和对象大量增加时,我们该怎么办?

设计模式可以是一个有用的指南。有用的设计模式是从实际实现中提炼出来的。这些模式旨在以可预测的方式解决给定形状的模式。当正确实现时,它们形成了一个预期的行为契约。这种可预测性和规律性(与其他模式的实现)有助于推理代码和更高层次的抽象。

在本章中,我们将了解如何使用常见的设计模式作为组织更大结构的蓝图。

使用模板函数定义步骤

模板是一种设计模式,它详细说明了给定操作集应执行的顺序;然而,模板本身并不概述步骤。当行为被划分为具有某些概念或副作用依赖性,需要按特定顺序执行的阶段时,此模式非常有用。

在这个食谱中,我们将了解如何使用模板函数设计模式。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做到这一点...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 09-01-defining-steps-with-template-functions 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件从 main.js 加载并运行 main 函数。

  4. 创建一个定义名为 Mission 的新抽象 classmain.js 文件:

// main.js 
class Mission { 
  constructor () { 
    if (this.constructor === Mission) { 
      throw new Error('Mission is an abstract class, must 
      extend'); 
    } 
  } 
}  
  1. 添加一个名为 execute 的函数,该函数调用三个实例方法——determineDestinationdeterminPayloadlaunch
// main.js 
class Mission { 
  execute () { 
    this.determinDestination(); 
    this.determinePayload(); 
    this.launch(); 
  } 
} 
  1. 创建一个扩展 Mission 类的 LunarRover 类:
// main.js 
class LunarRover extends Mission {} 
  1. 添加一个构造函数,将 name 分配给实例属性:
// main.js 
class LunarRover extends Mission 
  constructor (name) { 
    super(); 
    this.name = name; 
  } 
}  
  1. 实现 Mission.execute 调用的三个方法:
// main.js 
class LunarRover extends Mission {} 
  determinDestination() { 
    this.destination = 'Oceanus Procellarum'; 
  } 

  determinePayload() { 
    this.payload = 'Rover with camera and mass spectrometer.'; 
  } 

  launch() { 
    console.log(` 
Destination: ${this.destination} 
Playload: ${this.payload} 
Lauched! 
Rover Will arrive in a week. 
    `); 
  } 
}  
  1. 创建一个也扩展 Mission 类的 JovianOrbiter 类:
// main.js 
class LunarRover extends Mission {} 
constructor (name) { 
    super(); 
    this.name = name; 
  } 

  determinDestination() { 
    this.destination = 'Jovian Orbit'; 
  } 

  determinePayload() { 
    this.payload = 'Orbiter with decent module.'; 
  } 

  launch() { 
    console.log(` 
Destination: ${this.destination} 
Playload: ${this.payload} 
Lauched! 
Orbiter Will arrive in 7 years. 
    `); 
  } 
} 
  1. 创建一个 main 函数,该函数创建具体的任务类型并执行它们:
// main.js 
export function main() { 
  const jadeRabbit = new LunarRover('Jade Rabbit'); 
  jadeRabbit.execute(); 
  const galileo = new JovianOrbiter('Galileo'); 
  galileo.execute(); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 输出应如下所示:

它是如何工作的...

Mission抽象类定义了execute方法,该方法以特定顺序调用其他实例方法。你会注意到被调用的方法并非由Mission类定义。这个实现细节是扩展类的责任。这种抽象类的使用允许子类被利用抽象类定义的接口的代码所使用。

在模板函数模式中,定义步骤的责任在于子类。当它们被实例化,并调用execute方法时,这些步骤就会按照指定的顺序执行。

理想情况下,我们应该能够确保Mission.execute方法不会被任何继承类覆盖。覆盖此方法与模式相悖,并破坏了与之相关的契约。

这种模式对于组织数据处理管道非常有用。这些步骤以给定顺序发生的保证意味着,如果消除了副作用,实例可以更灵活地组织。实现类可以据此以最佳方式组织这些步骤。

使用构建器组装定制实例

之前的配方展示了如何组织类的操作。有时,对象初始化也可能很复杂。在这些情况下,利用另一个设计模式:构建器,可能很有用。

在这个配方中,我们将看到如何使用构建器来组织更复杂对象的初始化。

准备工作

此配方假设你已经有了一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何实现...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为09-02-assembling-instances-with-builders的新文件夹。

  3. 创建一个main.js文件,定义一个名为Mission的新class,它接受一个name构造函数参数并将其分配给实例属性。同时,创建一个describe方法,打印出一些详细信息:

// main.js 
class Mission { 
  constructor (name) { 
    this.name = name; 
  } 

  describe () { 
    console.log(`
      The ${this.name} mission will be launched by a
       ${this.rocket.name}
      rocket, and deliver a ${this.payload.name} to
      ${this.destination.name}. 
    `); 
  } 
   } 
  1. 创建名为DestinationPayloadRocket的类,这些类接收一个name属性作为构造函数参数并将其分配给实例属性:
// main.js 

class Destination { 
  constructor (name) { 
    this.name = name; 
  } 
} 

class Payload { 
  constructor (name) { 
    this.name = name; 
  } 
} 

class Rocket { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个MissionBuilder类,它定义了setMissionNamesetDestinationsetPayloadsetRocket方法:
// main.js 
class MissionBuilder { 

  setMissionName (name) { 
    this.missionName = name; 
    return this; 
  } 

  setDestination (destination) { 
    this.destination = destination; 
    return this; 
  } 

  setPayload (payload) { 
    this.payload = payload; 
    return this; 
  } 

  setRocket (rocket) { 
    this.rocket = rocket; 
    return this; 
  } 
} 
  1. 创建一个build方法,该方法使用适当的属性创建一个新的Mission实例:
// main.js 
class MissionBuilder { 
  build () { 
    const mission = new Mission(this.missionName); 
    mission.rocket = this.rocket; 
    mission.destination = this.destination; 
    mission.payload = this.payload; 
    return mission; 
  } 
}  
  1. 创建一个main函数,使用MissionBuilder创建一个新的任务实例:
// main.js 
export function main() { 
  // build an describe a mission 
  new MissionBuilder() 
    .setMissionName('Jade Rabbit') 
    .setDestination(new Destination('Oceanus Procellarum')) 
    .setPayload(new Payload('Lunar Rover')) 
    .setRocket(new Rocket('Long March 3B Y-23')) 
    .build() 
    .describe(); 
}  
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:http://localhost:8000/

  2. 你的输出应该如下所示:

图片

它是如何工作的...

构建器定义了分配所有相关属性的方法,并定义了一个build方法,确保每个方法都被适当地调用和分配。构建器类似于模板函数,但它们确保在返回实例之前,实例被正确配置。

因为MissionBuilder的每个实例方法都返回this引用,所以这些方法可以被链式调用。main函数的最后一行在build方法返回的新Mission实例上调用describe方法。

使用工厂复制实例

与构建器一样,工厂是组织对象构造的一种方式。它们在组织方式上与构建器不同。通常,工厂的接口是一个单一的功能调用。这使得工厂比构建器更容易使用,尽管可定制性较低。

在这个配方中,我们将看到如何使用工厂轻松复制实例。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为09-03-replicating-instances-with-factories的新文件夹。

  3. 复制或创建一个index.html,它加载并运行来自main.jsmain函数。

  4. 创建一个名为main.js的文件,定义一个名为Mission的新class。添加一个构造函数,它接受一个name构造函数参数并将其分配给实例属性。同时,定义一个简单的describe方法:

// main.js 
class Mission { 
  constructor (name) { 
    this.name = name; 
  } 

  describe () { 
    console.log(` 
The ${this.name} mission will be launched by a ${this.rocket.name} rocket, and 
deliver a ${this.payload.name} to ${this.destination.name}. 
    `); 
  } 
} 
  1. 创建三个名为DestinationPayloadRocketclass,它们接受name作为构造函数参数并将其分配给实例属性:
// main.js 
class Destination { 
  constructor (name) { 
    this.name = name; 
  } 
} 

class Payload { 
  constructor (name) { 
    this.name = name; 
  } 
} 

class Rocket { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个具有单个create方法的MarsMissionFactory对象,该方法接受两个参数:namerocket。此方法应使用这些参数创建一个新的Mission
// main.js 

const MarsMissionFactory = { 
  create (name, rocket) { 
    const mission = new Mission(name); 
    mission.destination = new Destination('Martian surface'); 
    mission.payload = new Payload('Mars rover'); 
    mission.rocket = rocket; 
    return mission; 
  } 
}  
  1. 创建一个main方法,创建并描述两个相似的使命:
// main.js 

export function main() { 
  // build an describe a mission 
  MarsMissionFactory 
    .create('Curiosity', new Rocket('Atlas V')) 
    .describe(); 
  MarsMissionFactory 
    .create('Spirit', new Rocket('Delta II')) 
    .describe(); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你的输出应该如下所示:

图片

它是如何工作的...

create方法接受创建新使命所需属性的一个子集。其余值由该方法本身提供。这使得工厂可以简化创建类似实例的过程。在main函数中,你可以看到创建了两个火星使命,它们只在名称和Rocket实例上有所不同。我们已经将创建实例所需值的数量减半。

这种模式可以帮助减少实例化逻辑。在这个配方中,我们通过识别共同属性,将这些属性封装在工厂函数体中,并使用参数提供剩余的属性,简化了不同类型使命的创建。这样,可以创建常用的实例形状,而无需额外的样板代码。

使用访问者模式处理结构

到目前为止我们所看到的模式组织了对象的构建和操作的执行。接下来我们将要查看的模式是专门用来遍历和执行层次结构上的操作的。

在这个菜谱中,我们将查看访问者模式。

准备工作

这个菜谱假设你已经有了一个允许你在浏览器中创建和运行 ES 模块的工作区。如果没有,请参阅前两章。

此外,这个菜谱假设你已经完成了之前的菜谱,使用构建器组装自定义实例。如果没有,请先完成那个菜谱。

如何做到...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 09-02-assembling-instances-with-builders 文件夹复制到新的 09-04-processing-a-structure-with-the-visitor-pattern 目录。

  3. 将名为 MissionInspector 的类添加到 main.js 中。创建一个 visitor 方法,该方法为以下类型调用相应的方法:MissionDestinationRocketPayload

// main.js 
/* visitor that inspects mission */ 
class MissionInspector { 
  visit (element) { 
    if (element instanceof Mission) { 
      this.visitMission(element); 
    } 
    else if (element instanceof Destination) { 
      this.visitDestination(element); 
    } 
    else if (element instanceof Rocket) { 
      this.visitRocket(element); 
    } 
    else if (element instanceof Payload) { 
      this.visitPayload(element); 
    } 
  } 
} 
  1. 创建一个 visitMission 方法,输出一个 ok 消息:
// main.js 
class MissionInspector { 
  visitMission (mission) { 
    console.log('Mission ok'); 
    mission.describe(); 
     } 
} 
  1. 创建一个 visitDestination 方法,如果目的地不在批准的列表中则抛出错误:
// main.js 
class MissionInspector { 
  visitDestination (destination) { 
    const name = destination.name.toLowerCase(); 

    if ( 
      name === 'mercury' || 
      name === 'venus' || 
      name === 'earth' || 
      name === 'moon' || 
      name === 'mars' 
    ) { 
      console.log('Destination: ', name, ' approved'); 
    } else { 
      throw new Error('Destination: '' + name + '' not approved      
      at this time'); 
    } 
     } 
} 
  1. 创建一个 visitPayload 方法,如果 payload 无效则抛出错误:
// main.js 
class MissionInspector { 
  visitPayload (payload) { 
    const name = payload.name.toLowerCase(); 
    const payloadExpr = /(orbiter)|(rover)/; 

    if ( payloadExpr.test(name) ) { 
      console.log('Payload: ', name, ' approved'); 
    } 
    else { 
      throw new Error('Payload: '' + name + '' not approved at 
      this time'); 
    } 
  } 
} 
  1. 创建一个 visitRocket 方法,输出一个 ok 消息:
// main.js 
class MissionInspector { 

  visitRocket (rocket) { 
    console.log('Rocket: ', rocket.name, ' approved'); 
  } 
} 
  1. Mission 类中添加一个 accept 方法,先对其组成部分调用 accept,然后告诉 visitor 访问当前实例:
// main.js 
class Mission { 

  // other mission code ... 

  accept (visitor) { 
    this.rocket.accept(visitor); 
    this.payload.accept(visitor); 
    this.destination.accept(visitor); 
    visitor.visit(this); 
  } 
  } 
  1. Destination 类中添加一个 accept 方法,告诉 visitor 访问当前实例:
// main.js 
class Destination { 

  // other mission code ... 

  accept (visitor) { 
    visitor.visit(this); 
    } 
  } 
  1. Payload 类中添加一个 accept 方法,告诉 visitor 访问当前实例:
// main.js 
class Payload { 

  // other mission code ... 

  accept (visitor) { 
    visitor.visit(this); 
    } 
  } 
  1. Rocket 类中添加一个 accept 方法,告诉 visitor 访问当前实例:
// main.js 
class Rocket { 

  // other mission code ... 

  accept (visitor) { 
    visitor.visit(this); 
    } 
  } 
  1. 创建一个 main 函数,使用构建器创建不同的实例,使用 MissionInspector 实例访问它们,并记录任何抛出的错误:
// main.js 
export function main() { 
  // build an describe a mission 
  const jadeRabbit = new MissionBuilder() 
    .setMissionName('Jade Rabbit') 
    .setDestination(new Destination('Moon')) 
    .setPayload(new Payload('Lunar Rover')) 
    .setRocket(new Rocket('Long March 3B Y-23')) 
    .build(); 

  const curiosity = new MissionBuilder() 
    .setMissionName('Curiosity') 
    .setDestination(new Destination('Mars')) 
    .setPayload(new Payload('Mars Rover')) 
    .setRocket(new Rocket('Delta II')) 
    .build(); 

  // expect error from Destination 
  const buzz = new MissionBuilder() 
    .setMissionName('Buzz Lightyear') 
    .setDestination(new Destination('Too Infinity And Beyond')) 
    .setPayload(new Payload('Interstellar Orbiter')) 
    .setRocket(new Rocket('Self Propelled')) 
    .build(); 

  // expect error from payload 
  const terraformer = new MissionBuilder() 
    .setMissionName('Mars Terraformer') 
    .setDestination(new Destination('Mars')) 
    .setPayload(new Payload('Terraformer')) 
    .setRocket(new Rocket('Light Sail')) 
    .build(); 

  const inspector = new MissionInspector(); 

  [jadeRabbit, curiosity, buzz, terraformer].forEach((mission) => 
   { 
    try { 
      mission.accept(inspector); 
    } catch (e) { console.error(e); } 
  }); 
} 
  1. 启动你的 Python 网络服务器,并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你的输出应该如下所示:

它是如何工作的...

访问者模式有两个组件。访问者处理主题对象,而主题告诉其他相关主题关于访问者的信息,以及当前主题何时应该被访问。

对于每个主题,accept 方法是必需的,以便接收有访问者存在的通知。然后该方法执行两种类型的方法调用。第一种是其相关主题上的 accept 方法。第二种是访问者上的 visitor 方法。通过这种方式,访问者通过主题之间的传递来遍历结构。

visitor 方法用于处理不同类型的节点。在某些语言中,这由语言级别的多态性处理。在 JavaScript 中,我们可以使用运行时类型检查来完成此操作。

访问者模式是处理对象分层结构的好选择,其中结构在事先未知,但已知主题的类型。

使用单例来管理实例

有时,存在一些资源密集型的对象。它们可能需要时间、内存、电池功率或网络使用,而这些资源可能不可用或不方便。管理实例的创建和共享通常很有用。

在这个菜谱中,我们将看到如何使用单例来管理实例。

准备工作

这个菜谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,导航到您的开发空间。

  2. 创建一个名为09-05-singleton-to-manage-instances的新文件夹。

  3. 复制或创建一个index.html文件,该文件从main.js加载并运行main函数。

  4. 创建一个名为Rocket的新class构造函数接受一个name构造参数并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor (name) { 
    this.name = name; 
  } 
}  
  1. 创建一个具有rockets属性的RocketManager对象。添加一个findOrCreate方法,通过name属性索引Rocket实例:
// main.js 
const RocketManager = { 
  rockets: {}, 
  findOrCreate (name) { 
    const rocket = this.rockets[name] || new Rocket(name); 
    this.rockets[name] = rocket; 
    return rocket; 
  } 
} 
  1. 创建一个main函数,该函数使用和没有使用管理器来创建实例。比较这些实例,看看它们是否相同:
// main.js 
export function main() { 
  const atlas = RocketManager.findOrCreate('Atlas V'); 
  const atlasCopy = RocketManager.findOrCreate('Atlas V'); 
  const atlasClone = new Rocket('Atlas V'); 

  console.log('Copy is the same: ', atlas === atlasCopy); 
  console.log('Clone is the same: ', atlas === atlasClone); 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您的输出应如下所示:

图片

它是如何工作的...

该对象存储了对实例的引用,通过name提供的字符串值进行索引。此映射在模块加载时创建,因此它贯穿整个程序的生命周期。然后单例能够查找对象,并返回与findOrCreate具有相同名称的实例。

节约资源和简化通信是使用单例的主要动机。为多个用途创建单个对象,在空间和时间需求方面比创建多个对象更有效。此外,为消息的传递拥有单个实例,使得程序不同部分之间的通信更容易。

如果单例依赖于更复杂的数据,可能需要更复杂的索引。

修改现有设计模式以适应不同的用例

模式并非来自更高层面的命令,它们的起源在于,并且是从现实世界的工程项目中提炼出来的。模式可以根据新的情况更好地进行修改。

在这个菜谱中,我们将看到如何修改工厂模式以简化创建任务。

准备工作

这个菜谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,导航到您的开发空间。

  2. 创建一个名为 09-06-modifying-existing-design-pattern-to-fit-differet-use-cases 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件定义了一个名为 Mission 的新 class。添加一个构造函数,它接受一个名为 name 的构造函数参数并将其分配给实例属性。此外,定义一个简单的 print 方法:

// main.js 
class Mission { 
  constructor (name) { 
    this.name = name; 
  } 

  describe () { 
    console.log(` The ${this.name} mission will be launched by a  
    ${this.rocket.name}, and deliver a ${this.payload.name} to 
    ${this.destination.name}. 
    `); 
  } 
}  
  1. 创建一个名为 Destination 的类。添加一个构造函数,它接受一个名为 name 的构造函数参数并将其分配给实例属性:
// main.js 
class Destination { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 Payload 的类。添加一个构造函数,它接受一个名为 name 的构造函数参数并将其分配给实例属性:
// main.js 
class Payload { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 Rocket 的类。添加一个构造函数,它接受一个 name 构造函数参数并将其分配给实例属性:
// main.js 
class Rocket { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 MissionProgramFactoryFn 的函数,它接受 rocketNamedestinationNamepayloadName 参数。这个函数应该返回一个函数,该函数接收一个 name 参数并返回一个新的 mission,包含所有以下属性:
// main.js 

function MissionProgramFactoryFn(rocketName, destinationName, payloadName) { 
  return (name) => { 
    const mission = new Mission(name); 
    mission.rocket = new Rocket(rocketName); 
    mission.destination = new Destination(destinationName); 
    mission.payload = new Payload(payloadName); 
    return mission; 
  } 
} 
  1. 创建一个名为 main 的函数,该函数创建两个程序工厂。使用实例创建并描述多个任务:
// main.js 
export function main() { 
  const marsRoverProgram = MissionProgramFactoryFn('AtlasV',
  'MartianSurface', 'Mars Rover'); 
  marsRoverProgram('Curiosity').describe(); 
  marsRoverProgram('Spirit').describe(); 

  const interstellarProgram = MissionProgramFactoryFn('Warp 
  Drive',
  'Vulcan', 'Dimplomatic Vessal'); 
  interstellarProgram('Enterprise E').describe(); 
  interstellarProgram('Defiant').describe(); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你的输出应如下所示:

它是如何工作的...

在前面的例子中,我们修改了工厂模式以使其更加灵活。而不是直接调用工厂函数,我们使用 MissionProgramFactoryFn 函数创建了新的工厂。实际上,我们创建了一个工厂的工厂!

JavaScript 函数内的变量对该函数包含的任何块都可用。因此,rocketNamedestinationNamepayloadName 的值可用于从 MissionProgramFactoryFn 返回的工厂函数的主体。这样,我们可以在不重复的情况下重用新实例的常见值。

从一个函数中返回一个函数称为二阶函数;这种模式在 JavaScript 中很常见。

将现有设计模式组合以适应不同的用例

修改和扩展模式并不意味着我们必须进入未知的领域。在解决新问题时,仍然建议使用已知模式。

在这个菜谱中,我们将看到如何结合两种模式以更好地适应给定的用例。

准备工作

这个菜谱假设你已经有一个工作区,它允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的工作区。

  2. 创建一个名为 09-07-combine-design-patters-to-fit-new-use-case 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件定义了一个名为 Mission 的新 class。创建一个将 name 参数分配给实例变量的构造函数。添加一个简单的 print 函数:

// main.js 
class Mission { 
  constructor (name) { 
    this.name = name; 
  } 

  describe () { 
    console.log(` 
The ${this.name} mission will be launched by a ${this.rocket.name} rocket, and 
deliver a ${this.payload.name} to ${this.destination.name}. 
    `); 
  } 
}  
  1. 创建一个名为Destination的类。创建一个构造函数,将name参数分配给实例变量:
// main.js 
class Destination { 
  constructor (name) { 
    this.name = name; 
  } 
}  
  1. 创建一个名为Payload的类。创建一个构造函数,将name参数分配给实例变量:
// main.js 
class Payload { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为Rocket的类。创建一个构造函数,将name参数分配给实例变量:
// main.js 
class Rocket { 
  constructor (name) { 
    this.name = name; 
  } 
} 
  1. 创建一个MissionBuilder类,该类定义了任务namepayloadrocket属性的设置器:
// main.js 
class MissionBuilder { 
  setMissionName (name) { 
    this.missionName = name; 
    return this; 
  } 

  setDestination (destination) { 
    this.destination = destination; 
    return this; 
  } 

  setPayload (payload) { 
    this.payload = payload; 
    return this; 
  } 

  setRocket (rocket) { 
    this.rocket = rocket; 
    return this; 
  } 
}  
  1. 添加一个build函数,组装所有这些属性:
// main.js 
class MissionBuilder { 
  build () { 
    const mission = new Mission(this.missionName); 
    mission.rocket = this.rocket; 
    mission.destination = this.destination; 
    mission.payload = this.payload; 
    return mission; 
  } 
} 
  1. 创建一个MarsMissionFactory对象,该对象接受namerocket参数,并使用MissionBuilder来组装一个新的任务:
// main.js 

const MarsMissionFactory = { 
  create (name, rocket) { 
    return new MissionBuilder() 
      .setMissionName(name) 
      .setDestination(new Destination('Martian Surface')) 
      .setPayload(new Payload('Mars Rover')) 
      .setRocket(rocket) 
      .build() 
  } 
} 
  1. 创建一个main函数,创建并描述几个火星任务的实例:
// main.js 
export function main() { 
  // build an describe a mission 
  MarsMissionFactory 
    .create('Curiosity', new Rocket('Atlas V')) 
    .describe(); 
  MarsMissionFactory 
    .create('Spirit', new Rocket('Delta II')) 
    .describe(); 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您的输出应如下所示:

它是如何工作的...

MarsMissionFactory接收missionnamerocket属性的值,然后将剩余的值提供给一个构建器实例。这缩小了创建新mission所需属性的范围,同时仍然使用构建器接口。

这种组合模式而不是修改模式的方法在许多用例中更可取。与更成熟的库代码一样,更知名的模式比自定义模式有更好的定义的契约和更可预测的行为。它们的熟悉性使得新来者更容易理解。

第十章:与数组一起工作

在本章中,我们将介绍以下食谱:

  • 使用 Array#find 和 Array#filter 在数组中查找值

  • 使用 Array#slice 获取数组的子集

  • 使用 Array#every 和 Array#some 测试数组值

  • 使用 Array.map 生成值

  • 使用 Array.reduce 转换数据

  • 使用解构提取数组成员

  • 使用 rest 运算符获取数组的头和尾

  • 使用扩展运算符合并数组

简介

数组几乎是每种语言的基本数据结构,JavaScript 也不例外。这些集合的常见任务包括搜索、分割和合并等。直到最近,这意味着要编写很多循环,或者包括实现这些循环的大型库。然而,ES6 包括对 Array API 的扩展,这使得这些任务变得更加容易。

使用 Array#find 和 Array#filter 在数组中查找值

在数组中搜索项目时,有时我们搜索单个项目,有时我们搜索符合某些标准的多项项目。Array#find 和 Array#filter 函数旨在简化这一点。

在本食谱中,我们将探讨如何使用这两个函数在数组中定位元素。

准备中

本食谱假设您已经有一个允许您在浏览器中创建和运行 ES 模块的 workspace。如果您没有,请参阅前两章。

如何做...

  1. 打开您的命令行应用程序,并导航到您的 workspace。

  2. 创建一个名为 10-01-using-find-and-filter 的新文件夹。

  3. 复制或创建一个 index.html,它从 main.js 加载并运行一个 main 函数。

  4. 创建一个名为 main.js 的文件,定义一个新的抽象 class,命名为 Rocket。在构造时分配一个 name 实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 创建一个 main 函数,构建几个 Rocket 实例:
// main.js 
export function main() { 
  const saturnV = new Rocket('US: Saturn V'); 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  const longMarch = new Rocket('CN: Long March'); 
  const rockets = [saturnV, falconHeavy, longMarch]; 
} 
  1. 使用 find 方法定位第一个美国 Rocket
// main.js 
export function main () { 
  // ... 
  const firstUSRocket = rockets.find((rocket) => 
  rocket.name.indexOf('US') === 0); 
  console.log('First US Rocket: ', firstUSRocket.name); 
} 
  1. 使用 filter 方法查找所有 American Rockets 实例:
// main.js 
export function main () { 
  // ... 
  const allUSRockets = rockets.filter((rocket) => 
  rocket.name.indexOf('US') === 0); 
  console.log('All US Rockets: ',allUSRockets); 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您将看到以下输出显示:

图片

它是如何工作的...

findfilter 方法都接受一个具有单个参数(数组中的项)的函数,并返回一个布尔值。如果函数返回 true,则当前项是匹配项。如果返回 false,则不是。find 方法在找到第一个匹配项后终止并返回该值。filter 方法从当前数组创建一个包含所有匹配项的新数组。

在上述食谱中,我们看到 find 方法返回第一个被识别为美国的 Rocket,而 filter 方法返回所有被识别为美国的 rockets

使用 Array#slice 获取数组的子集

有时,我们希望根据数组索引而不是数组索引处的数组内容来获取数组的子集。在本菜谱中,我们将探讨如何使用 slice 获取数组的子集。

准备工作

本菜谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的开发空间。

  2. 创建一个名为 10-02-using-slice-to-get-subset 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个 main 函数,创建几个 Rocket 实例并将它们放入数组中:
// main.js 
export function main() { 
  const saturnV = new Rocket('US: Saturn V'); 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  const soyuz = new Rocket('USSR: Soyuz'); 
  const dongFeng = new Rocket('CN: Dong Feng'); 
  const longMarch = new Rocket('CN: Long March'); 
  const rockets = [saturnV, falconHeavy, soyuz, dongFeng, 
  longMarch]; 
} 
  1. 根据国家将 Rockets 数组划分为三个子集:
// main.js 
export function main() { 
  //....
  const americanRockets = rockets.slice(0, 2); 
  const sovietRockets = rockets.slice(2, 3); 
  const chineseRockets = rockets.slice(3, 5); 
  console.log('American Rockets: ', americanRockets); 
  console.log('Soviet Rockets: ', sovietRockets); 
  console.log('Chinese Rockets: ', chineseRockets); 
}  
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

图片

工作原理...

slice 方法接受两个参数,一个起始索引和一个结束索引。结束索引是非包含的。这意味着新集合将包括从起始索引到结束索引之间的元素,包括起始索引处的元素,但不包括结束索引处的元素。

这可能有点令人困惑,但可以这样考虑。假设起始索引是 2,结束索引是 3。这两个数字之间的差是 1,结果子集中只有一个元素。对于索引 0 和 2,差是 2,结果子集中将有两个元素。

使用 Array#every 和 Array#some 测试数组值

有时,我们需要了解整个数组的信息,而不仅仅是单个元素的信息,例如“是否有任何元素满足某些标准?”或“所有元素是否满足某些标准?”。

在本菜谱中,我们将探讨如何使用 someevery 方法来测试数组。

准备工作

本菜谱假设您已经有一个工作空间,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的开发空间。

  2. 创建一个名为 10-03-using-every-and-some-to-test-values 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个 main 函数,创建几个 Rocket 实例并将它们放入数组中:
// main.js 
export function main() { 
  const saturnV = new Rocket('US: Saturn V'); 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  const soyuz = new Rocket('USSR: Soyuz'); 
  const dongFeng = new Rocket('CN: Dong Feng'); 
  const longMarch = new Rocket('CN: Long March'); 
  const rockets = [saturnV, falconHeavy, soyuz, dongFeng, 
  longMarch]; 
} 
  1. 使用 every 方法来确定所有成员是否是 Rocket 类的实例:
// main.js 
export function main() { 
  //... 
  const allAreRockets = rockets.every((rocket) => rocket 
  instanceof Rocket); 
  console.log('All are Rockets: ', allAreRockets) 
} 
  1. 使用 every 方法来确定所有成员是否是 American Rockets
// main.js 
export function main() { 
  //... 
  const allAmerican = rockets.every((rocket) => 
  rocket.name.indexOf('US:') === 0); 
  console.log('All rockets are American: ', allAmerican); 
} 
  1. 使用 some 方法来确定是否有任何成员是 American Rockets
// main.js 
export function main() { 
  //... 
  const someAmerican = rockets.some((rocket) =>
  rocket.name.indexOf('US:') === 0); 
  console.log('Some rockets are American: ', someAmerican); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

图片

工作原理...

everysome 方法与 filterfind 方法类似。它们接受一个函数,该函数接收一个数组元素作为参数并返回一个布尔值。这个布尔值的真值被 everysome 方法用来减少到一个单一值。some 方法在任何一个回调返回 true 时立即返回 trueevery 方法遍历所有元素,只有当所有回调都返回 true 时才返回 true

使用 Array.map 生成值

其他数组操作旨在生成新值。这可以是数组元素的属性或为每个元素计算的其他任何值。map 方法遍历每个元素并将值收集到一个新数组中。

在本食谱中,我们将探讨如何使用 map 创建一个包含新值的新数组。

准备工作

本食谱假设您已经有一个允许您在浏览器中创建和运行 ES 模块的开发空间。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的开发空间。

  2. 创建一个名为 10-04-map-to-produce-values 的新文件夹。

  3. 创建一个名为 main.js 的文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个 main 函数,创建几个 Rocket 实例并将它们放入一个数组中:
// main.js 
export function main() { 
  const saturnV = new Rocket('US: Saturn V'); 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  const soyuz = new Rocket('USSR: Soyuz'); 
  const dongFeng = new Rocket('CN: Dong Feng'); 
  const longMarch = new Rocket('CN: Long March'); 
  const rockets = [saturnV, falconHeavy, soyuz, dongFeng, 
  longMarch]; 
} 
  1. 使用 map 方法,并返回每个元素的国籍的字符串表示:
// main.js 
export function main() { 
  //... 
  const nationalities = rockets.map((rocket) => { 
    if (rocket.name.indexOf('USSR:') === 0) { 
      return 'Soviet'; 
    } 
    if (rocket.name.indexOf('CN:') === 0) { 
      return 'Chinese'; 
    } 
    if (rocket.name.indexOf('US:') === 0) { 
      return 'American'; 
    } 

    return 'unknown'; 
  }); 

  console.log('Nationalities:', nationalities) 
} 
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出:

图片

工作原理...

Map 方法与我们所看到的几个其他方法类似。它接受一个函数,该函数接收一个数组元素作为参数并返回某个值。这些值被收集到一个新数组中,该新数组由 map 方法返回。

使用 Array.reduce 转换数据

map 方法非常适合创建直接映射到现有数组元素的数组。然而,有时所需的结果具有不同的形状。为此,我们可以使用 reduce 方法将值累积到新的形式中。

在本食谱中,我们将探讨如何使用 reduce 方法来转换数据。

准备工作

本食谱假设您已经有一个允许您在浏览器中创建和运行 ES 模块的开发空间。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的开发空间。

  2. 创建一个名为 10-05-reduce-to-transform-data 的新文件夹。

  3. 创建一个名为 main.js 的文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个包含国籍字符串数组的 main 函数:
// main.js 
export function main() { 
  const nationalities = [ 
    'American', 
    'American', 
    'Chinese', 
    'American', 
    'Chinese', 
    'Chinese', 
    'Soviet', 
    'Soviet' 
  ]; 
} 
  1. 使用 reduce 方法来统计不同的国籍:
// main.js 
export function main() { 
  //... 
const nationalityCount = nationalities.reduce((acc, nationality) => { 
    acc[nationality] = acc[nationality] || 0; 
    acc[nationality] ++; 
    return acc; 
  }, {}); 

  console.log('Nationalities:', nationalityCount); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

与我们之前看到的其他方法一样,reduce方法遍历数组中的每个元素。然而,它接受一组不同的参数。第一个参数是一个接收两个参数的函数,一个累加器和当前元素。这个函数的结果是新的累加值。

第二个参数是累加器的初始值。

在这个菜谱中,累加器被初始化为一个空对象。然后函数使用当前数组值作为键,并增加该键的计数器。这样,我们统计每个键出现的次数。

我们可以看到,与map不同,结果数据形状与初始数组不同。

使用解构提取数组成员

直接索引数组相对简单。语法对所有但最新手开发者来说都很熟悉。然而,同样熟悉的是索引错误。这意味着数组或集合被错误地索引了一个位置。在某些情况下,这会导致立即可识别的错误。其他时候,它会导致更微妙错误。

在这个菜谱中,我们将探讨如何使用解构语法提取数组的成员。

准备工作

这个菜谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为10-06-extract-array-members-with-destructuring的新文件夹。

  3. 创建一个main.js文件,定义一个名为Rocket的新class,它接受一个构造函数参数name并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个包含火箭数组的main函数:
// main.js 
export function main() { 
  const rockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy'), 
    new Rocket('USSR: Soyuz'), 
    new Rocket('CN: Dong Feng'), 
    new Rocket('CN: Long March') 
  ] 
}  
  1. 使用解构语法将每个成员分配给局部变量:
// main.js 
export function main() { 
  //... 
  const [ 
    saturnV, 
    falconHeavy, 
    soyuz, 
    dongFeng, 
    longMarch 
  ] = rockets; 

  console.log(saturnV, falconHeavy, soyuz, dongFeng, longMarch); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

上述菜谱展示了如何使用解构语法为数组中的元素创建单个变量。解构语法反映了相应数组的索引。因此,变量名将与数组中相应位置的元素关联。零^(th)位置的名称将在数组的开始处分配一个值。位置 1 的名称将分配下一个值,依此类推。

因此,我们可以看到上面我们为rockets数组中的每个元素命名。提取的每个元素的值与相应的火箭相匹配。

使用剩余操作符获取数组的头和尾

使用解构来挑选元素很方便,但我们并不总是想要取出每个元素。一个常用的模式是将数组的零元素分配给一个变量,其余的元素分配给另一个变量。这通常被称为数组的头和尾。

在这个食谱中,我们将看看如何使用剩余操作符来获取数组的头和尾。

准备工作

这个食谱假设你已经有一个允许你在浏览器中创建和运行 ES 模块的工作区。如果没有,请参考前两章。

如何做...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为 10-07-get-head-and-tail-from-array 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个包含火箭数组的 main 函数:
// main.js 
export function main() { 
  const rockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy'), 
    new Rocket('USSR: Soyuz'), 
    new Rocket('CN: Dong Feng'), 
    new Rocket('CN: Long March') 
  ] 
}  
  1. 使用解构语法和剩余操作符来获取数组的头和尾:
// main.js 
export function main() { 
  //... 
  const [saturnV, ...otherRockets] = rockets; 
  console.log(saturnV); 
  console.log(otherRockets) 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

第一个元素 saturnV 的工作方式与前面的食谱相同。剩余操作符表示剩余的元素应该分配给 otherRockets 变量。由于它不一定是单个元素,这将是一个数组。

应该注意的是,剩余操作符必须是解构语法的最后一个成员。如果它后面跟着一个逗号,你将收到一个解析错误。

使用展开操作符合并数组

我们已经看到了如何使用一些新的语法来从数组中提取元素。同样,也有一些新的工具用于合并数组。如我们所见,展开操作符的使用与剩余操作符类似。

在这个食谱中,我们将看看如何使用展开操作符来合并数组。

准备工作

这个食谱假设你已经有一个允许你在浏览器中创建和运行 ES 模块的工作区。如果没有,请参考前两章。

如何做...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为 10-08-combine-arrays-using-spread 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个 main 函数,包含多个按国籍划分的火箭数组,以及一个独立的 Rocket 变量:
// main.js 
export function main() { 
  const usRockets= [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy') 
  ]; 

  const sovietRocket = new Rocket('USSR: Soyuz'); 

  const chineseRockets = [ 
    new Rocket('CN: Dong Feng'), 
    new Rocket('CN: Long March') 
  ]; 
}  
  1. 使用解构语法和展开操作符将火箭合并成一个单独的数组:
// main.js 
export function main() { 
  //... 
  const rockets = [...usRockets, sovietRocket, 
  ...chineseRockets]; 
  console.log(rockets); 
} 
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

我认为剩余操作符的方式是将其集合的成员 展开 到当前集合中。在先前的菜谱中,这意味着 usRocketschineseRockets 的成员被展开到新数组 rockets 中。

这种扩展意味着它们可以在创建新数组时与独立的 sovietRocket 在相同的语法级别上进行引用。这种将元素组合成新结构的过程有时被称为 结构化,与 解构化 术语相对应。

第十一章:与 Maps 和 Symbols 一起工作

在本章中,我们将介绍以下配方:

  • 使用 Symbol 创建局部实例

  • 使用 Symbol.for 创建全局实例

  • 使用 Symbol 模拟枚举

  • 从 Map 中设置和删除条目

  • 从现有数据创建 Map

  • 创建一个包装 Map 以与特定复杂类型一起工作的类

  • 从 WeakMap 中设置和删除条目

  • 从现有数据创建 WeakMap

  • 创建一个使用 WeakMap 与特定复杂类型一起工作的类

简介

我们看到了如何使用 ECMAScript 经典语义简洁地表达数据与操作之间更复杂的关系。我们还看到了如何利用现有类型(对象和数组)的扩展 API。然而,ECMAScript 还有更多要提供。在新的类型中包括 SymbolMap 以及 Map 的表亲 WeakMap。这些类型在一定程度上可以在 JavaScript 的早期版本中模拟,但现在它们是现成的并且有原生支持。

本章中的配方将展示这些类型的一些用法,包括一起使用和单独使用。

使用 Symbol 创建局部实例

单独使用 Symbol 并不是特别有用,但它们作为其他数据结构的键非常有用。它们非常适合作为键,因为可以限制对其值的访问。这些比较有两种工作方式。我们可以创建局部符号,它们是唯一的,可以在初始化后重新创建,以及全局符号,可以通过其构造函数值进行引用。

在这个配方中,我们将看看如何使用 Symbol 作为函数来创建局部符号。这意味着即使使用相同的参数,每个实例也将是新的。

准备工作

此配方假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做到这一点...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 11-01-local-symbols 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件定义一个 main 函数,使用相同的参数创建几组符号。按照以下方式打印出它们的相等性:

// main.js 
export function main() {
    const usLaunchLocation = Symbol.for('Kennedy Space Center');
    const duplicateLaunchLocation = Symbol.for('Kennedy Space 
    Center');
    console.log(usLaunchLocation, duplicateLaunchLocation);
    console.log('Identical launch locations: ', usLaunchLocation 
    === duplicateLaunchLocation);
    const rocketNumber = Symbol.for(5);
    const duplicateRocketNumber = Symbol.for(5);
    const stringDuplicateRocketNumber = Symbol.for("5");
    console.log(rocketNumber, duplicateRocketNumber, 
    stringDuplicateRocketNumber);
    console.log('Identical rocket numbers: ', rocketNumber === 
    duplicateRocketNumber);
    console.log(
        'Identical string rocket numbers: ',
        rocketNumber ===    stringDuplicateRocketNumber
    );
}
  1. 使用 Symbol.keyFor 记录火箭数字 Symbolkey
// main.js
export function main() {
    // ...
    console.log(Symbol.keyFor(rocketNumber), 
    Symbol.keyFor(stringDuplicateRocketNumber));
    // print type
    console.log(
        typeof Symbol.keyFor(rocketNumber),
        typeof Symbol.keyFor(stringDuplicateRocketNumber)
    )
}
  1. 启动您的 Python 网络服务器,并在您的浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

图片

它是如何工作的...

当使用 Symbol.for 方法创建一个 Symbol 时,返回的实例可能是一个预存在的实例。当我们使用相同的字符串和数字值创建一个 Symbol 时,我们可以看到这一点。当提供一个数字作为字符串时,我们甚至可以看到相同的实例。

当打印出键的类型时,我们可以看到为什么数字匹配,即使参数是字符串也是如此。当我们检索一个数值键时,它会被转换为字符串,因此与数字的字符串表示形式等效。

使用 Symbol.for 创建全局实例

我们已经看到了如何创建唯一的符号,用作局部上下文中的键。然而,有时我们希望能够与数据结构进行交互。在这种情况下,符号也可以被用来工作。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参考前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的工作区。

  2. 创建一个名为 11-02-symbol-for-global 的新文件夹。

  3. 复制或创建一个 index.html,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,该文件定义了一个创建几组 Symbols 的 main 函数,使用 Symbol.for,带有字符串和数字参数。比较数字符号与字符串形式的数字:

// main.js 
export function main() { 
  const usLaunchLocation = Symbol.for('Kennedy Space Center'); 
  const duplicateLaunchLocation = Symbol.for('Kennedy Space 
  Center'); 
  console.log(usLaunchLocation, duplicateLaunchLocation); 
  console.log('Identical launch locations: ', usLaunchLocation 
  === duplicateLaunchLocation); 

  const rocketNumber = Symbol.for(5); 
  const duplicateRocketNumber = Symbol.for(5); 
  const badDuplicateRocketNumber = Symbol.for('5'); 
  console.log(rocketNumber, duplicateRocketNumber, 
  badDuplicateRocketNumber); 
  console.log('Identical rocket numbers: ', rocketNumber === 
  duplicateRocketNumber); 
  console.log('Identical bad rocket numbers: ', rocketNumber 
  === duplicateRocketNumber); 
}  
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出:

图片

它是如何工作的...

slice 方法接受两个参数,一个起始索引和一个结束索引。结束索引是非包含的。这意味着新集合将包括起始索引和结束索引之间的元素,包括起始索引处的元素,但不包括结束索引处的元素。

这可能看起来有点令人困惑,但可以这样想,考虑起始索引是两个,结束索引是三个。这两个数字之间的差值是一,结果子集中只有一个元素。对于索引 02,差值是两个,结果子集中将有两个元素。

使用 Symbol 模拟枚举

我们已经看到了如何创建可以在全局访问的 Symbol,以及那些不能在初始上下文之外访问的 Symbol。现在,我们将看到如何使用它们来创建在 JavaScript 早期版本中实际上不可能实现的功能。

在这个食谱中,我们将使用局部 Symbol 来模拟许多其他语言中可用的类型,枚举。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参考前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的工作区。

  2. 创建一个名为 11-03-symbols-simulate-enums 的新文件夹。

  3. 创建一个名为 main.js 的文件,该文件定义了一个名为 LaunchSite 的新 object,对象的属性值应该是本地的 Symbols

// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  WHITE_SANDS: Symbol('White Sands Missile Range'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome'), 
  BROGLIO: Symbol('Broglio Space Center'), 
  VIKRAM_SARABHAI: Symbol('Vikram Sarabhai Space Centre') 
}   
  1. 创建一个 main 函数并比较枚举条目的值:
// main.js 
export function main() { 
  console.log("Kennedy Space Center Site: ", LaunchSite.KENNEDY_SPACE_CENTER); 
  console.log("Duplicate String: ", LaunchSite.KENNEDY_SPACE_CENTER === 'Kennedy Space Center'); 
  console.log("Duplicate Symbol: ", LaunchSite.KENNEDY_SPACE_CENTER === Symbol('Kennedy Space Center')); 
  console.log("Duplicate Global Symbol: ", LaunchSite.KENNEDY_SPACE_CENTER === Symbol.for('kennedy Space Center')); 
}  
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出:

它是如何工作的...

如前所述,局部 Symbol 实例每次创建时都是唯一的。因此,我们无法在 main 函数中重新创建 Symbol 值。这意味着任何期望从该枚举中获取值的代码都不能用等效值强制转换。

枚举类型的一个有用用途是作为选项。想象一个选项对象,其中一个选项需要能够接受多个值(比如动画缓动)。字符串值可以完成这项工作,但很容易出错。使用枚举,函数的用户必须引用该枚举;这在阅读时更不脆弱且更清晰。

在映射中设置和删除条目

本章的其余部分将专注于 ECMAScript 中添加的新数据结构。在一定程度上,它们可以在 ES5 中模拟。然而,原生的支持和明确的命名使得利用这些特性的代码更高效且更清晰。

在这个菜谱中,我们将查看使用 setdelete 方法添加和删除 Map 条目的基本方法。

准备工作

这个菜谱假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参考前两章。

如何做到这一点...

  1. 打开你的命令行应用程序并导航到你的 workspace。

  2. 创建一个名为 11-04-set-and-delete-from-map 的新文件夹。

  3. 创建一个名为 main.js 的文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个包含不同发射站的枚举:
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
}  
  1. 创建一个 main 函数。在该函数中,使用 setdelete 方法来操作发射站到火箭的条目:
// main.js 
export function main() { 
  const rocketSiteMap = new Map(); 

  rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, new Rocket('US: 
  Saturn V')); 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  rocketSiteMap.set(LaunchSite.WHITE_SANDS, falconHeavy); 
  console.log(rocketSiteMap.get(LaunchSite.KENNEDY_SPACE_CENTER)); 
  console.log(rocketSiteMap.get(LaunchSite.WHITE_SANDS)); 

  rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, new Rocket('US: 
  Space Shuttle')); 
  rocketSiteMap.delete(LaunchSite.WHITE_SANDS); 
  console.log(rocketSiteMap.get(LaunchSite.KENNEDY_SPACE_CENTER)); 
  console.log(rocketSiteMap.get(LaunchSite.WHITE_SANDS));} 

  1. 启动你的 Python 网络服务器并在你的浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

映射是一个 keyvalue 的配对。当调用 Map 实例方法时,键引用值。这种配对是一对一的;这意味着每个键只能有一个值。因此,当我们设置另一个火箭到 KENNEDY_SPACE_CENTER 键时,旧值将被替换。

delete 方法删除与 key 对应的条目。因此,在 delete 之后,该特定条目是未定义的。

从现有数据创建映射

我们刚刚看到了如何逐个向映射中添加值。然而,这可能会很繁琐。例如,如果我们正在处理一个可能非常大或事先未知的数据集,那么用函数调用初始化映射而不是几百或几千个值会更好。

在这个菜谱中,我们将查看如何使用预存数据创建一个新的映射。

准备工作

这个菜谱假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参考前两章。

如何做到这一点...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 11-05-create-map-from-data 的新文件夹。

  3. 创建一个名为 main.js 的文件,该文件定义了一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个各种发射场的枚举:
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
}  
  1. 创建一个 main 函数。在该函数中,创建一个包含发射场和火箭键值对的映射:
// main.js 
export function main() { 
  const rocketSites = [ 
    [ LaunchSite.KENNEDY_SPACE_CENTER, new Rocket('US: Saturn 
    V'),], 
    [ LaunchSite.WHITE_SANDS, new Rocket('US: Falcon Heavy') ], 
    [ LaunchSite.BAIKONUR, new Rocket('USSR: Soyuz') ], 
    [ LaunchSite.JUIQUAN, new Rocket('CN: Long March') ] ] 

  const rocketSiteMap = new Map(rocketSites); 
  console.log(rocketSiteMap) 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

图片

它是如何工作的...

如前所述,映射是 keyvalue 之间的配对。Map 构造函数期望一个可迭代的键值对集合。在前面的配方中,我们传递了一个二维数组。外部维度是包含多个条目的可迭代。

内部维度是键值对。键值对的第一成员是发射场。第二成员是 value(在我们的情况下,是一个 Rocket)。Map 构造函数遍历提供的条目并在每个之间创建配对。

创建一个包装 Map 以处理特定复杂类型的类

当处理大量集合时,了解在挑选成员时可以期望哪种类型的对象是很不错的。通常,JavaScript 集合是异构的,这意味着可以使用任何类型。在 Map 的情况下,这意味着 keyvalue 可以采用任何类型。

在这个配方中,我们将看看如何创建 Map 的包装类,以便控制 Map 中使用的类型。

准备工作

此配方假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参阅前两章。

如何做...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 11-06-create-class-to-wrap-map 的新文件夹。

  3. 创建一个名为 main.js 的文件,该文件定义了一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个名为 RocketSiteMap 的类文件,该文件创建一个新的映射并将其作为构造函数中的实例属性分配:
// main.js 
class RocketSiteMap { 
  constructor () { 
    this.map = new Map(); 
     }    
   } 
  1. 添加一个检查 keyvalue 参数类型的 set 方法。如果参数类型不正确,则该方法应抛出错误,否则将键值对作为映射上的条目设置:
// main.js 
class RocketSiteMap { 
  set (site, rocket) { 
    if (!(rocket instanceof Rocket)) { 
      throw new Error('Value of `RocketMap` must be of type 
      `Rocket`'); 
    } 
    else if (typeof site !== 'symbol') { 
      throw new Error('Key of `RocketMap` must be of type 
      `Symbol`'); 
    } 

    this.map.set(site, rocket); 
  }   
   } 
  1. 添加一个返回映射中 key 的条目的 get 方法:
// main.js 
class RocketSiteMap { 
  get (key) { 
    return this.get(key); 
   } 
  1. 创建一个各种发射场的枚举:
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
}  
  1. 创建一个 main 函数。尝试将各种 keyvalue 对设置到 RocketMap 的实例中:
// main.js 
export function main() { 
  const rocketSiteMap = new RocketSiteMap(); 
  rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, new Rocket('US: 
  Saturn V')); 
  rocketSiteMap.set(LaunchSite.WHITE_SANDS, new Rocket('US: Falcon 
  Heavy')); 
  console.log(rocketSiteMap) 

  try { 
    rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, 'Buzz 
    Lightyear'); 
  } catch (e) { 
    console.error(e); 
  } 

  try { 
    rocketSiteMap.set('Invalid Lanch Site', new Rocket('Long 
    March')); 
  } catch (e) { 
    console.error(e); 
  } 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/

  2. 您应该看到以下输出:

图片

它是如何工作的...

set 方法的实现中,我们可以看到正在检查参数的类型。Symbol 作为一种类型,没有构造函数,所以我们不能使用 instanceof 操作符,但 typeof 操作符返回一个 symbol 字符串,我们可以检查它。Rocket 实例的行为类似于我们在其他食谱中看到的其他实例,并且可以像它们一样进行检查。

当将错误类型作为参数传递给 set 时,其中一个条件将触发并抛出错误。

从 WeakMap 中设置和删除条目

我们已经看到了如何在各种情况下使用 Maps。在 ECMAScript 中还有一个新的类,其行为非常相似,但有一些有用的属性。WeakMap,就像 Map 一样,是一个键值数据结构。

在本食谱中,我们将探讨如何使用 setdelete 方法向 WeakMap 添加和删除元素。我们还将看到它们与 Map 类的区别。

准备工作

本食谱假设您已经有一个允许您在浏览器中创建和运行 ES 模块的 workspace。如果您没有,请参阅前两章。

如何操作...

  1. 打开您的命令行应用程序并导航到您的 workspace。

  2. 创建一个名为 11-07-set-and-delete-from-weakmap 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个各种发射场的 enum
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
} 
  1. 创建一个 main 函数。在该函数中,使用 setdelete 方法来操作发射场到火箭的条目。尝试使用 Symbol 作为键:
// main.js 
export function main() { 
  const falconHeavy = new Rocket('US: Falcon Heavy'); 
  const rocketSiteMap = new WeakMap(); 

  rocketSiteMap.set(new Rocket('US: Saturn V'), 
  LaunchSite.KENNEDY_SPACE_CENTER); 
  rocketSiteMap.set(falconHeavy, 
  LaunchSite.KENNEDY_SPACE_CENTER); 
  console.log(rocketSiteMap) 

  rocketSiteMap.delete(falconHeavy); 
  console.log(rocketSiteMap) 

  // try to set with a symbol; expect error 
  rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, falconHeavy); 
} 
  1. 启动您的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 您应该看到以下输出:

图片

它是如何工作的...

Map 的实例类似,WeakMap 实例是 keyvalue 之间的配对。然而,关键的区别在于可以用于键的类型。Map 可以使用任何类型作为键。WeakMap 只能将 Object 类型的值作为键。这有助于 WeakMap 更高效。

WeakMap 可以更高效的原因与内存管理和垃圾回收有关。考虑一下,Map 条目必须保留在程序的整个生命周期内。由于它们可以使用原始类型作为键(布尔值、字符串、数字和符号),这些值可以被重新创建,并且那些 Map 条目可以在任何时间被引用。

与之相比,WeakMap 的键只能为 Object 类型。Object 值不能被重新创建;具有相同值的对象仍然是不同的实例。这意味着 WeakMap 的条目只能在使用键的引用可用时访问;一旦该引用丢失,条目就再也无法访问。

由于键值没有任何现有的引用,因此条目不再可访问。这意味着条目可以从内存中释放(保留未使用的值没有意义)。这允许垃圾回收器释放该内存。

请参阅 Mozilla 文档以获取有关 WeakMap 的更多信息:

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap.

从现有数据创建 WeakMap

我们刚刚看到了如何将值逐个添加到 WeakMap 中,以及为什么它与 Map 不同。然而,逐个创建 WeakMap 可能会很繁琐。

在这个配方中,我们将看看如何创建一个新的 Map,其中包含现有的数据。

准备工作

这个配方假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参阅前两章。

如果你还不熟悉 WeakMap 类,请参阅 从 WeakMap 中设置和删除条目 的配方。

如何操作...

  1. 打开你的命令行应用程序并导航到你的 workspace。

  2. 创建一个名为 11-08-create-weakmap-from-data 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个各种发射站的枚举:
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
}  
  1. 创建一个 main 函数。在该函数中,创建一个包含发射站和火箭键值对的映射:
// main.js 
export function main() { 
  const rocketSites = [ 
    [ new Rocket('US: Saturn V'), LaunchSite.KENNEDY_SPACE_CENTER 
    ], 
    [ new Rocket('US: Falcon Heavy'), LaunchSite.WHITE_SANDS ], 
    [ new Rocket('USSR: Soyuz'), LaunchSite.BAIKONUR ], 
    [ new Rocket('CN: Dong Feng'), LaunchSite.JUIQUAN ], 
    [ new Rocket('CN: Long March'), LaunchSite.JUIQUAN ] ]; 

  const rocketSiteMap = new WeakMap(rocketSites); 
  console.log(rocketSiteMap); 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

没有惊喜,WeakMap 构造函数遵循与 Map 相同的接口。唯一的区别是对 key 的类型限制。构造函数的参数是一个二维数组,其中外维是条目列表。内维表示键值对。内维的第一个成员是 key,第二个是 value

创建一个使用 WeakMap 与特定复杂类型一起工作的类

就像 Map 一样,了解 WeakMap 集合中预期的类型可能会有所帮助。key 类型受到轻微的限制,但仍然相当宽松,对值的类型没有限制。

在这个配方中,我们将看看如何创建一个包装类来控制 WeakMap 中使用的类型。

准备工作

这个配方假设你已经有一个允许你在浏览器中创建和运行 ES 模块的 workspace。如果你没有,请参阅前两章。

如果你还不熟悉 WeakMap 类,请参阅 从 WeakMap 中设置和删除条目 的配方。

如何操作...

  1. 打开你的命令行应用程序并导航到你的 workspace。

  2. 创建一个名为 11-09-create-class-to-wrap-weakmap 的新文件夹。

  3. 创建一个 main.js 文件,该文件定义了一个名为 Rocket 的新 class,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
} 
  1. 创建一个名为 RocketSiteMap 的类文件,该文件创建一个新的映射并将其作为实例属性分配给构造函数:
// main.js 
class RocketSiteMap { 
  constructor () { 
    this.map = new WeakMap(); 
     }    
   } 
  1. 添加一个检查 keyvalue 参数类型的 set 方法。如果参数类型不正确,则该方法应抛出异常,否则将键值对作为映射的条目设置:
// main.js 
class RocketSiteMap { 
set (rocket, site) { 
    if (!(rocket instanceof Rocket)) { 
      throw new Error('Key of `RocketMap` must be of type 
      `Rocket`'); 
    } 
    else if (typeof site !== 'symbol') { 
      throw new Error('Values of `RocketMap` must be of type 
      `Symbol`'); 
    } 

    this.map.set(rocket, site); 
  } 

  get (key) { 
    return this.get(key); 
     } 
   } 
  1. 添加一个 get 方法,该方法从映射中返回 key 的条目:
// main.js 
class RocketSiteMap { 
  get (key) { 
    return this.get(key); 
   } 
  1. 创建一个包含各种发射场枚举:
// main.js 
const LaunchSite = { 
  KENNEDY_SPACE_CENTER: Symbol('Kennedy Space Center'), 
  JUIQUAN: Symbol('Jiuquan Satellite Launch Center'), 
  WHITE_SANDS: Symbol('Jiuquan Satellite Launch Center'), 
  BAIKONUR: Symbol('Baikonur Cosmodrome') 
}  
  1. 创建一个 main 函数。尝试将各种 keyvalue 对设置到 RocketMap 的实例中:
// main.js 
export function main() { 
  const rocketSiteMap = new RocketSiteMap(); 
  rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, new 
  Rocket('US: 
  Saturn V')); 
  rocketSiteMap.set(LaunchSite.WHITE_SANDS, new Rocket('US: 
  Falcon 
  Heavy')); 
  console.log(rocketSiteMap) 

  try { 
    rocketSiteMap.set(LaunchSite.KENNEDY_SPACE_CENTER, 'Buzz 
    Lightyear'); 
  } catch (e) { 
    console.error(e); 
  } 

  try { 
    rocketSiteMap.set('Invalid Lanch Site', new Rocket('Long 
    March')); 
  } catch (e) { 
    console.error(e); 
  } 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下链接:

    http://localhost:8000/.

  2. 你应该看到以下输出:

它是如何工作的...

set 方法的实现中,我们可以看到正在检查参数的类型。Symbol 作为一种类型,没有构造函数,所以我们不能使用 instanceof 操作符,但 typeof 操作符返回一个可以检查的符号字符串。Rocket 实例的行为类似于我们在其他菜谱中看到的其他实例,可以像它们一样进行检查。

当将错误类型作为参数传递给 set 方法时,其中一个条件将触发,并抛出错误。

第十二章:使用集合

本章将涵盖以下食谱:

  • 向集合中添加和删除项

  • 从现有数据创建一个 Set

  • 从 WeakSet 中添加和删除项

  • 从现有数据创建一个 WeakSet

  • 查找两个集合的并集

  • 查找两个集合的交集

  • 查找两个集合之间的差异

  • 创建一个包装 Set 的类以处理更复杂的数据类型

简介

对于我们的最后一章,我们将探讨两种更多的新相关类型。SetWeakSet,像MapWeakMap一样,是其他值的集合。然而,SetWeakSet不是在值对之间创建关系,而是在集合的所有条目之间创建关系。这些数据结构确保没有重复的条目。如果一个新项评估为与另一个成员相等,它将不会被添加到Set中。

本章中的食谱将展示如何使用集合类实现不同的行为。

向集合中添加和删除项

我们将从涉及Set的最简单任务开始。在本食谱中,我们将探讨如何使用相应实例方法向Set中添加和删除项。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为12-01-add-remove-from-set的新文件夹。

  3. 复制或创建一个index.html,它从main.js加载并运行一个main函数。

  4. 创建一个main.js文件,定义一个main函数。在该函数中,创建一个新的Set实例,然后从中添加和删除一些项:

// main.js
export function main() {
    const rocketSet = new Set();
    rocketSet.add('US: Saturn V');
    rocketSet.add('US: Saturn V');
    rocketSet.add('US: Falcon Heavy');
    console.log(rocketSet);
    rocketSet.delete('US: Falcon Heavy');
    console.log(rocketSet);
}
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 你将看到以下输出:

图片

它是如何工作的...

集合是一组数据。但它的行为与更熟悉的Array类型不同。像数学集合一样,Set实例旨在只包含一个元素副本。也就是说,如果你有一个数字的Array和一个数字的SetArray可以包含数字138多次,但Set只能包含一个副本。

集合的成员资格评估方式类似于===运算符。在我们的例子中,你可以看到US: Saturn V只被添加到集合中一次,尽管它被作为add的参数提供了两次。接下来,你可以看到US: Falcon Heavy在被最初添加后就被移除了。随后,集合中只有一个成员。

要了解更多关于集合的信息,请访问以下链接的 Mozilla 开发者文档:

developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set.

从现有数据创建一个 Set

我们刚刚看到了如何将值逐个添加到集合中。然而,这可能会很繁琐。例如,如果我们正在处理一个可能非常大或事先未知的数据集,使用函数调用初始化映射会比数百或数千个映射更方便。

在本食谱中,我们将探讨如何使用现有数据创建一个新的 Set。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 12-02-create-set-from-data 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,定义一个名为 main 的函数,它有一个字符串数组作为参数。使用该数组作为构造函数参数创建一个新的集合:

// main.js 
export function main() { 
  const rockets = [ 
    'US: Saturn V', 
    'US: Falcon Heavy', 
    'USSR: Soyuz', 
    'CN: Long March', 
    'US: Saturn V', 
    'US: Saturn V' 
  ];

  const rocketSet = new Set(rockets); 
  console.log(rockets); 
}  
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 你应该看到以下输出:

工作原理...

在前面的食谱中,我们看到如何使用现有数据创建一个 SetSet 的构造函数接受一个 iterable 作为参数。最熟悉的 iterable 是一个数组。在 Set 的情况下,iterable(数组)中的每个元素按顺序添加到集合中。正如我们在前面的食谱中提到的,Set 的成员资格是通过与 === 操作符类似的比较来确定的。集合不允许重复值。因此,我们只看到输出 <entries> 部分中的单个 US: Saturn V

从 WeakSet 中添加和删除项目

现在,我们将查看相应的弱数据结构,WeakSet。在本食谱中,我们将探讨如何使用相应实例方法向 WeakSet 添加和删除项目,以及一些关于成员资格的限制。

准备工作

本食谱假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,导航到你的工作区。

  2. 创建一个名为 12-03-add-remove-from-weak-set 的新文件夹。

  3. 复制或创建一个 index.html 文件,该文件加载并运行来自 main.jsmain 函数。

  4. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,它接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
}    
  1. 创建一个 main 函数,包含一些 Rocket 实例和一个 WeakMap 实例。将实例添加到集合中并从中删除:
// main.js 
export function main() {
const saturnV = new Rocket('US: Saturn V');
const falconHeavy = new Rocket('US: Falcon Heavy');
const rocketSet = new WeakSet();
rocketSet.add(saturnV);
rocketSet.add(saturnV);
rocketSet.add(falconHeavy);
console.log(rocketSet);
rocketSet.delete(falconHeavy);
console.log(rocketSet);
// throw error
rocketSet.add('Saturn V');
}
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 你将看到以下输出:

工作原理...

WeakSet的成员资格评估方式类似于===运算符。查看前面的内容,我们可以看到名为US: Saturn V的两个Rocket实例被添加到集合中。这当然是因为集合不是比较名称属性,而是对象实例。因此,我们将看到两个而不是一个(唯一的名称)或三个(添加次数)US: Saturn V条目。

接下来,你可以看到US: Falcon Heavy在最初添加后被移除。随后,集合中不再有那个成员。

当尝试向WeakSet添加字符串时,会抛出错误。像WeakMap的键一样,WeakSet的元素必须是对象。这样,WeakSet只能对其条目保持弱引用。因此,当其他引用超出作用域时,可以为条目分配的内存被释放。

从现有数据创建 WeakSet

我们刚刚看到了如何从现有数据创建 Set。相关的类WeakSet可以以类似的方式创建,但对其成员资格有限制。在这个配方中,我们将查看如何从现有数据创建WeakSet以及对其成员资格的限制。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为12-04-create-weak-set-from-data的新文件夹。

  3. 创建一个名为main.js的文件,定义一个名为Rocket的新类,该类接受一个构造函数参数name并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
}    
  1. 创建一个包含火箭实例数组的main函数。从数组中创建一个新的WeakSet。尝试向WeakSet添加一个字符串:
// main.js 
export function main() { 
  const rockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Saturn V'), 
    new Rocket('USSR: Soyuz') , 
    new Rocket('CN: Long March') 
  ] 

  const rocketSet = new WeakSet(rockets); 
  console.log(rockets);
}  
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 你应该看到以下输出:

图片

它是如何工作的...

就像 Set 构造函数一样,WeakSet构造函数也接受一个可迭代对象。数组中的每个元素依次被添加。我们将注意到多个实例具有相同的name属性。这当然是因为它们是独立的Rocket实例,尽管它们具有相同的名称值。

查找两个集合的并集

现在我们已经很好地掌握了集合,是时候开始执行一些集合操作了。集合是无序的事物组;你可能想要做的是将两个组合并成一个。这个操作称为两个集合的并集。如果一个元素存在于两个集合中的任何一个中,那么它就在两个集合的并集中。

在这个配方中,我们将查看如何创建两个Set实例的并集。

准备工作

这个配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参考前两章。

如何操作...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为12-05-set-union的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   }  
  1. 创建一个名为 union 的函数,该函数接受两个集合参数:
// main.js 
function union (set1, set2) {}  
  1. 创建一个 result 集合。遍历两个集合实例,并将每个 entry 添加到结果集合中:
// main.js 
function union (set1, set2) { 
  const result = new Set(); 

  set1.forEach((entry) => result.add(entry)); 
  set2.forEach((entry) => result.add(entry)); 

  return result; 
} 
  1. 创建一个 main 函数。创建几个具有重叠成员的集合。从两个集合的并集中退出:
// main.js 
export function main() { 
  const usRockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy') 
  ]; 
  const americanSet = new Set(usRockets); 
  console.log('American Set', americanSet); 

  const allRockets = usRockets.concat([ 
    new Rocket('USSR: Soyuz'), 
    new Rocket('CN: Long March') 
  ]); 

  const fullSet = new Set(allRockets); 
  console.log('Full Set', fullSet); 

  console.log('Union', union(americanSet, fullSet)); 
}  
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 您应该看到以下输出:

它是如何工作的...

在前面的食谱中,我们依赖于 Set 的属性来实现 union 操作。如前所述,集合不存储重复条目。因此,在创建并集时,我们不必担心将重复项添加到结果集合中,因为这一点由类为我们处理。只需遍历所有成员并添加它们就足以创建并集集合。

查找两个集合的交集

接下来,我们将看看如何找出两个集合共有的元素。这个操作称为两个集合的 intersection。如果一个元素存在于两个集合的交集中,那么它就存在于这两个集合中。

在本食谱中,我们将了解如何创建两个 Set 实例的交集。

准备工作

本食谱假设您已经有一个工作区,允许您在浏览器中创建和运行 ES 模块。如果您没有,请参考前两章。

如何操作...

  1. 打开您的命令行应用程序,并导航到您的 workspace。

  2. 创建一个名为 12-06-set-intersection 的新文件夹。

  3. 创建一个 main.js 文件,定义一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   }  
  1. 创建一个名为 intersection 的函数,该函数接受两个 set 参数:
// main.js 
function intersection (set1, set2) {}  
  1. 创建一个 result 集合。遍历第一个 set 实例。如果它也出现在第二个 set 中,则将每个 entry 添加到结果 set 中:
// main.js 
function intersection (set1, set2) { 
  const result = new Set(); 

  set1.forEach((entry) => { 
    if (set2.has(entry)) { 
      result.add(entry); 
    } 
  }); 

  return result; 
} 
  1. 创建一个 main 函数。创建几个具有重叠成员的集合。输出两个集合的交集:
// main.js 
export function main() { 
  const usRockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy') 
  ]; 
  const americanSet = new Set(usRockets); 
  console.log('American Set', americanSet); 

  const allRockets = usRockets.concat([ 
    new Rocket('USSR: Soyuz'), 
    new Rocket('CN: Long March') 
  ]); 

  const fullSet = new Set(allRockets); 
  console.log('Full Set', fullSet); 

  console.log('Intersetion', intersection(americanSet, fullSet)); 
  }  
  1. 启动您的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/.

  2. 您应该看到以下输出:

它是如何工作的...

两个集合的交集定义为同时出现在两个集合中的所有元素。在本食谱中,我们使用 has 方法来实现 intersection 操作。我们遍历第一个 Set 的元素,并检查第二个集合是否包含每个元素。如果第二个 Set 包含该元素,则 has 方法将返回 true。如果此方法返回 true,则我们知道该元素存在于两个 Set 实例中,并将其添加到新的交集。

查找两个集合之间的差异

我们已经看到了如何使用并集操作组合两个集合,以及如何使用交集操作找到它们的共同元素。下一个逻辑步骤是看看如何找出一个集合中另一个集合没有的元素。这个操作称为两个集合的difference。如果一个元素在两个集合的差集中,那么它位于第一个集合中,但不位于第二个集合中。

在本配方中,我们将探讨如何找到两个Set实例之间的差异。

准备中

本配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如何做...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为12-07-set-difference的新文件夹。

  3. 创建一个名为main.js的文件,该文件定义了一个名为Rocket的新类,它接受一个构造函数参数name并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   }  
  1. 创建一个名为difference的函数,该函数接受两个set参数:
// main.js 
function intersection (set1, set2) {}  
  1. 创建一个result集合。遍历第一个集合实例。如果每个条目不在第二个set中,则将其添加到结果集合中:
// main.js 
function difference(set1, set2) { 
  const result = new Set(); 

  set1.forEach((entry) => { 
    if (!set2.has(entry)) { 
      result.add(entry) 
    } 
  }); 

  return result; 
} 
  1. 创建一个main函数。创建几个具有重叠成员的集合。记录两个集合的差异:
// main.js 
export function main() { 
  const usRockets = [ 
    new Rocket('US: Saturn V'), 
    new Rocket('US: Falcon Heavy') 
  ]; 
  const americanSet = new Set(usRockets); 
  console.log('American Set', americanSet); 

  const allRockets = usRockets.concat([ 
    new Rocket('USSR: Soyuz'), 
    new Rocket('CN: Long March') 
  ]); 

  const fullSet = new Set(allRockets); 
  console.log('Full Set', fullSet); 

  console.log('Difference 1', difference(americanSet, fullSet)); 
  console.log('Difference 2', difference(fullSet, americanSet)); 
}  
  1. 启动你的 Python 网络服务器,并在浏览器中打开以下 URL:

    http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

在前面的配方中,我们依赖于Set类上的has方法来实现difference操作。差分的元素必须出现在第一个集合中,但不出现在第二个集合中。在遍历第一个集合时,我们知道该元素是第一个集合的一部分。接下来,我们只需使用has方法检查它是否在第二个集合中。如果此方法返回 false,则我们知道该元素在差集中。

差分与其他两个操作(并集和交集)的一个重要区别是它不是交换的,也就是说,参数的顺序很重要。你可以在前面的配方中看到,差分函数的结果取决于哪个集合被从另一个集合中区分出来。

创建一个包装 Set 的类以处理更复杂的数据类型

了解我们正在处理的数据类型是有价值的。WeakSet对成员资格有一些限制,但正如你可能知道的,对象可能会有很大的变化。

在本配方中,我们将探讨如何创建Map的包装类,以控制Map中使用的类型。

准备中

本配方假设你已经有一个工作区,允许你在浏览器中创建和运行 ES 模块。如果你没有,请参阅前两章。

如果你不太熟悉WeakMap类,请参阅从 WeakMap 中设置和删除条目配方。

如何做...

  1. 打开你的命令行应用程序,并导航到你的工作区。

  2. 创建一个名为 12-08-create-class-to-wrap-set 的新文件夹。

  3. 创建一个 main.js 文件,该文件定义了一个名为 Rocket 的新类,该类接受一个构造函数参数 name 并将其分配给实例属性:

// main.js 
class Rocket { 
  constructor(name) { 
    this.name = name; 
  } 
   } 
  1. 创建一个名为 RocketSet 的类文件,该文件创建一个新的映射并将其作为实例属性分配给构造函数:
// main.js 
class RocketSet { 
  constructor () { 
    this.set = new WeakSet(); 
     }    
   } 
  1. 添加一个 add 方法,该方法检查 keyvalue 参数的类型。如果参数类型不正确,则抛出异常;否则,将这对作为映射条目设置:
// main.js 
class RocketSet { 
  add (rocket) { 
    if (!(rocket instanceof Rocket)) { 
      throw new Error('Members of `RocketSet` must be of type 
      `Rocket`'); 
    } 

    this.set.add(rocket); 
      } 
   } 
  1. 添加一个 has 方法,如果包含的集合中有该条目,则返回 true
// main.js 
class RocketSiteMap { 
  has (rocket) { 
    return this.set.has(rocket); 
     } 
   }
} 
  1. 创建一个 main 函数。尝试将各种键值对设置到 RocketSet 实例中:
// main.js 
export function main() { 
  const rocketSet = new RocketSet(); 
  const saturnV = new Rocket('US: Saturn V');
  const falconHeavy = new Rocket('US: Falcon Heavy');
  const longMarch = new Rocket('Long March') ;
  rocketSet.add(saturnV); 
  rocketSet.add(falconHeavy); 
  rocketSet.add(longMarch); 
  console.log(rocketSet) ;

  console.log('Set has Saturn V ',rocketSet.has(saturnV));
  console.log('Set has Falcon Heavy 
  ',rocketSet.has(falconHeavy));
  console.log('Set has Long March ',rocketSet.has(longMarch));

  try { 
    rocketSet.add('Buzz Lightyear'); 
  } catch (e) { 
    console.error(e); 
  } 
} 
  1. 启动你的 Python 网络服务器并在浏览器中打开以下 URL:

    http://localhost:8000/

  2. 你应该看到以下输出:

图片

它是如何工作的...

add 方法的实现中,我们可以看到正在检查参数的类型。Rocket 实例的行为类似于我们在其他菜谱中看到的其他实例,并且可以像其他实例一样进行检查。当将不正确的类型作为 add 的参数传递时,其中一个条件将触发并抛出错误。

我们不需要为 has 方法检查类型;它不会修改集合,并且如果参数不是 Rocket,它将返回 false

posted @ 2025-09-26 22:10  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报