七、高级 JavaScript

学习目标

在本章结束时,你将能够:

  • 使用 Node.js REPL 测试简单的脚本
  • 构造对象和数组并修改它们的内容
  • 使用对象方法和操作符来获取关于对象的信息
  • 创建简单的 JavaScript 类和继承自其他类的类
  • 使用先进的内置 m 方法从数学,RegEx,日期,和字符串
  • 在 JavaScript 中使用 Array、Map 和 Set 方法操作数据
  • 实现符号、迭代器、生成器和代理

在本章中,我们将使用 JavaScript 中的数组、类和对象,然后我们将在常见的 JavaScript 类中使用继承和内置方法来简化我们的代码并使其高度可重用。

简介

在为中型到大型项目(10 个以上文件)编写 JavaScript 代码时,理解该语言提供的所有可能特性是很有帮助的。 使用已经存在的东西总是比重新发明轮子更容易、更快。 这些内置方法不仅可以帮助您执行基本功能,还可以帮助提高代码的可读性和可维护性。 这些内置方法的范围从基本计算到开发人员每天都要面对的复杂数组和字符串操作。 通过使用这些内置方法,我们可以减少代码大小并帮助提高应用的性能。

JavaScript 通常用作函数式语言,但你可以使用它面向对象编程(OOP)。 近年来,许多新特性(如类)被添加到该语言中,以响应日益增长的对 JavaScript 的需求,以完成更复杂和数据驱动的任务。 虽然仍然可以使用函数原型创建 JavaScript,但许多开发人员已经不再这样做了,因为它提供了更接近的语法,类似于流行的 OOP 语言,如 c++、Java 和 c#。

在本章中,我们将探索 JavaScript 提供给我们的大量内置方法。 我们将使用 Node.jsREPL(Read-Eval-Print Loop)来测试我们的代码,因为这不需要我们在磁盘上创建任何文件或调用任何特殊命令。

语言特性 ES5、ES6、ES7、ES8、ES9 支持

在深入研究这些惊人的语言特性之前,让我们先看看 JavaScript 的不同版本。 目前,你经常遇到的大多数仍然支持旧浏览器的网站都使用 ES5。 截至 2019 年,许多主流浏览器已经添加了对 ES6 的支持。 以后的版本将只有最低限度的浏览器支持。 因为我们将在 Node.js 运行时中运行和测试我们的代码,所以只要我们使用 Node.js 的最新 LTS(长期支持)版本,我们就不必担心版本兼容性。 关于本章将使用的材料,这里是运行时需要支持的最小 ES 版本的细分:

Figure 7.1: Minimum required ES version

Fig7.1:最低要求的 ES 版本

在本章中,我们不会切换运行时,但在未来,最好在开始之前检查一下你要开发的运行时的语言支持。

W 在 Node.js REPL 中工作

在本章中,我们不会做任何太复杂的事情,所以我们将在Node.jsREPL 中编写代码。 这允许我们在开始编码之前测试一些想法,而不需要创建任何文件。 在我们开始之前,请确保您的计算机上安装了 Node.js,并打开了终端应用。

执行 Node.js REPL

每个 node .js 安装都包含一个节点可执行文件,允许您运行本地 JavaScript 文件或启动 REPL。 要以 REPL 的形式运行 Node.js 可执行文件,您所需要做的就是在您喜欢的终端中输入node命令,而不需要任何参数。 要测试我们的 Node.js 安装,你可以运行node -v命令:

Figure 7.2: Testing the Node.js installation

图 7.2:测试 Node.js 安装

如果您看到这样的输出,这意味着您已经正确地安装了Node.js

请注意

这个命令输出当前运行的Node.js运行时版本,所以它也是检查当前版本的一个很好的方法。 对于这本书,我们将使用当前的 LTS,即 v10.16.0。

在验证 node .js 安装后,要在 REPL 模式下运行 node 命令,你需要做的就是在命令提示符中输入node:

Figure 7.3: Running the node command in REPL mode

图 7.3:在 REPL 模式下运行 node 命令

如果您看到一个游标在等待您的输入,恭喜—您已经成功进入 Node.js 的 REPL 模式! 从现在开始,您可以开始在提示符中输入代码并按 Enter 来计算它。

Array JavaScript 操作

在 JavaScript 中创建数组和修改它的内容非常容易。 与其他语言不同,在 JavaScript 中创建数组不需要指定数据类型或大小,因为这些可以在以后请求时更改。

要创建一个 JavaScript 数组,使用以下命令:

const jsArray = [];

注意,在 JavaScript 中,不需要定义数组中项的大小或类型。

要创建具有预定义元素的数组,使用以下命令:

const foodList = ['sushi', 'fried chicken', 21];

要访问和修改数组中的项,使用以下命令:

const sushi = foodList[0];
foodList[2] = 'steak';

在访问数组时,这与其他编程语言非常相似。

练习 37:创建和修改 Array

在本练习中,我们将创建一个简单的数组,并使用 REPL 研究它的值。 创建数组的语法与许多其他脚本语言非常相似。 我们将以两种方式创建一个singers数组:一种是使用Array构造函数,另一种是使用数组字面量。 一旦创建了数组,我们就可以操作数组的内容。 让我们开始:

  1. 使用数组字面量方法创建一个空数组,然后测试是否成功创建:
  2. Now, we will use the Array constructor to do the same. While they yield the same result, the constructor allows more flexibility:

    ```js

    let exampleArray2 = new Array(); ```

    js => undefined

    ```js

    Array.isArray(exampleArray2); ```

    js => true

    注意,我们没有使用typeof来检查数组的类型,因为在 JavaScript 中,数组是一种对象类型。 如果我们在刚刚创建的数组上使用typeof,会得到一个意想不到的结果:

    ```js

    let exampleArray3 = []; ```

    js => undefined

    ```js

    typeof exampleArray3 ```

    js => 'object'

  3. Create arrays with a predefined size and items. Note that JavaScript arrays will automatically resize as you add items to the array:

    ```js

    let exampleArray4 = new Array(6) ```

    js => undefined

    ```js

    exampleArray4 ```

    js => [ <6 empty items> ]

    js or

    ```js

    let singers = new Array(6).fill('miku') ```

    js => undefined

    ```js

    singers ```

    js => [ 'miku', 'miku', 'miku', 'miku', 'miku', 'miku' ]

    如你所见,我们有一个初始大小为6的数组。 我们还使用了fill方法来预定义数组中的所有项。 当我们想要使用数组来跟踪应用中的标志时,这是非常有用的。

  4. 0:

    ```js

    singers[0] = 'miku' ```

    js => 'miku'

    ```js

    singers ```

    js => [ 'miku' ]

  5. 为 JavaScript 数组指定任意索引。 没有赋值的索引为:undefined:

    ```js

    singers[3] = 'luka' ```

    js => 'luka'

    ```js

    singers[1] ```

    js => undefined

  6. js > singers[singers.length - 1] = 'rin'

    js => 'rin'

    ```js

    singers ```

    js => [ 'miku', 'miku', 'miku', 'miku', 'miku', 'rin' ]

我们已经学习了如何在 JavaScript 中定义数组。 这些数组的行为类似于其他语言,它们也是自动扩展的,所以您不必担心手动调整数组的大小。 在下一个练习中,我们将学习如何向数组中添加项。

练习 38:添加和删除项目

从 JavaScript 数组中添加和删除项非常容易,在许多需要积累大量项的应用中,我们都必须这样做。 在这个练习中,我们将修改前面创建的现有的singers数组。 让我们开始:

  1. 从一个空数组开始:

    ```js

    let singers = []; ```

    js => undefined

  2. Add a new item to the end of an array using push:

    ```js

    singers.push('miku') ```

    js => 1

    ```js

    singers ```

    js => [ 'miku' ]

    push方法总是将项目添加到数组的末尾,即使你的数组中的项目是undefined:

    ```js

    let food = new Array(3) ```

    js => undefined

    ```js

    food.push('burger') ```

    js => 4

    ```js

    food ```

    js => [ <3 empty items>, 'burger' ]

    正如你在前面的代码中所看到的,如果你有一个预定义大小的数组,使用push将扩展数组并将其添加到数组的末尾,而不是仅仅将其添加到数组的开头

  3. js > singers.push('me')

    js => 2

    ```js

    singers ```

    js => [ 'miku', 'me' ]

    ```js

    singers.pop() ```

    js => 'me'

    ```js

    singers ```

    js => [ 'miku' ]

  4. js > singers.unshift('rin')

    js => 2

    ```js

    singers ```

    js => [ 'rin', 'miku' ]

  5. js > singers.shift()

    js => 'rin'

    ```js

    singers ```

    js => [ 'miku' ]

这些在大型应用中非常有用,例如如果您正在构建一个处理图像的简单 web 应用。 当请求传入时,您可以将图像数据、作业 ID 甚至客户机连接推送到一个数组,这意味着 JavaScript 数组可以是任何类型。 您可以让另一个工人调用数组上的pop来检索作业,然后处理它们。

Exercise 39:数组中的各项信息

在这个练习中,我们将介绍获取数组中项信息的各种基本方法。 当我们处理需要操作数据的应用时,这些函数非常有用。 让我们开始:

  1. js > let foods = []

    js => undefined

    ```js

    foods.push('burger') ```

    js => 1

    ```js

    foods.push('fries') ```

    js => 2

    ```js

    foods.push('wings') ```

    js => 3

  2. 查找一个项目的索引:

    ```js

    foods.indexOf('burger') ```

    js => 0

  3. js > foods.length

    js => 3

  4. Remove an item from a certain index in the array. We will do this by storing the position of the item we want to remove into a variable position. After we know where we want to remove the item, we can call array.splice to remove it:

    ```js

    let position = foods.indexOf('burger') ```

    js => undefined

    ```js

    foods.splice(position, 1) // splice(startIndex, deleteCount) ```

    js => [ 'burger' ]

    ```js

    foods ```

    js => [ 'fries', 'wings' ]

    请注意

    array.splice也可用于在特定索引处插入/替换数组项。 稍后我们将详细讨论这个函数。 在使用它时,我们为它提供了两个参数。 第一个告诉 splice 从哪里开始,下一个告诉它从开始位置删除多少项。 因为我们只想删除该索引处的项,所以我们为它提供了 1。

在这个练习中,我们探索了获取关于数组的更多信息的方法。 在构建应用时,尝试定位特定项的索引非常有用。 使用这些内置方法非常有用,因为您不需要自己遍历数组来查找项。 在下一个活动中,我们将使用用户的 ID 构建一个简单的用户跟踪器。

活动 8:创建用户跟踪器

假设你正在建立一个网站,你想要追踪目前有多少人在浏览它。 为了做到这一点,您决定在后端保留一个用户列表。 当用户打开您的网站时,您将更新列表以包括该用户,当该用户关闭您的网站时,您将从列表中删除该用户。

对于这个活动,我们将有一个名为users的列表,它存储一个字符串列表,以及一些帮助函数来帮助存储和删除列表中的用户。

为了做到这一点,我们需要定义一个函数,它获取我们的用户列表并根据我们的喜好修改它。

执行以下步骤来完成此活动:

  1. 创建Activity08.js文件。
  2. 定义一个logUser函数,该函数将用户添加到提供的userList参数中,并确保没有添加重复的参数。
  3. 定义一个userLeft函数。 它将从参数中提供的userList参数中删除用户。
  4. 定义一个numUsers函数,该函数返回当前列表中的用户数量。
  5. Define a function called runSite. This will be used to test our implementation.

    请注意

    这个活动的解决方案可以在 607 页找到。

在这个活动中,我们探索了一种在 JavaScript 中使用数组来完成某些任务的方法。 我们可以使用它来跟踪项目列表,并使用内置方法来添加和删除项目。 我们之所以看到user3user5user6是因为这些用户从未被删除。

Object 操作 JavaScript

用 JavaScript 创建基本对象非常容易,而且对象在每个 JavaScript 应用中都使用。 JavaScript 对象还包括一组供您使用的内置方法。 这些方法在我们编写代码时非常有用,因为它使 JavaScript 开发变得非常简单和有趣。 在本节中,我们将研究如何在代码中创建对象,以及如何使用它们来最大化它们的潜力。

要在 JavaScript 中创建一个对象,使用以下命令:

const myObj = {};

通过使用{}符号,我们定义了一个空对象并将其赋给变量名。

在我们的应用中,可以使用对象来存储多个键值对:

myObj.item1 = 'item1';
myObj.item2 = 12;

如果我们想要访问这个值,这也很容易:

const item = myObj.item1;

在 JavaScript 中,创建对象并不意味着必须遵循特定的模式。 可以在对象中放置任意数量的属性。 只要确保没有对象键被复制:

> dancers = []
=> undefined
> dancers.push({ name: 'joey', age: 30 })
=> undefined

注意,新对象的语法与 JSON 表示法非常相似。 有些时候,我们需要确切地知道对象中包含哪些信息。

你可以用一些属性创建一个对象用户:

> let myConsole = { name: 'PS4', color: 'black', price: 499, library: []}
=> undefined

为了获得所有的属性名,你需要使用keys方法,如下所示:

> Object.keys(myConsole)
=> [ 'name', 'color', 'price', 'library' ]

我们还可以测试属性是否存在。 让我们检查一下没有定义的属性:

> if (myConsole.ramSize) {
... console.log('ram size is defined.');
... }
> undefined

现在,让我们检查一下我们之前定义的属性:

> if (myConsole.price) {
... console.log('price is defined.');
... }
> price is defined.

这是测试对象中是否存在属性的一种非常简单的方法。 在许多应用中,这经常用于检查字段是否存在,如果不存在,则设置默认值。 记住,在 JavaScript 中,空字符串、空数组、数字 0 和其他假值都会被if语句计算为false。 在下面的练习中,我们将尝试创建一个包含大量信息的对象,并从中输出非常有用的信息。

练习 40:用 JavaScript 创建和修改对象

在本练习中,我们将在数组中存储对象,并通过修改对象来修改数组。 然后,我们将检查如何使用对象的属性来访问它。 我们将继续使用前面定义的singers数组,但这次我们将使用对象,而不是仅存储字符串列表。 让我们开始:

  1. singers数组设置为空数组:

    ```js

    singers = [] ```

    js => undefined

  2. Push 对象到数组:

    ```js

    singers.push({ name: 'miku', age: 16 }) ```

    js => undefined

  3. Modify the name property of the first object inside the array:

    ```js

    singers[0].name = 'Hatsune Miku' ```

    js => 'Hatsune Miku'

    ```js

    singers ```

    js => [ { name: 'Hatsune Miku', age: 16 } ]

    修改对象中的值非常简单; 例如,你可以给属性赋任何值,但它不会停止。 还可以添加不属于对象的属性,以扩展其信息。

  4. Add a property called birthday to the object:

    ```js

    singers[0].birthday = 'August 31' ```

    js => 'August 31'

    ```js

    singers ```

    js => [ { name: 'Hatsune Miku', age: 16, birthday: 'August 31' } ]

    要向现有对象添加属性,只需为属性名指定一个值。 如果这个属性不存在,它将创建这个属性。 您可以为属性、函数、数组或其他对象指定任何值。

  5. Read the property in the object by executing the following code:

    ```js

    singers[0].name ```

    js => 'Hatsune Miku'

    js or

    ```js

    const propertyName = 'name' ```

    js => undefined

    ```js

    singers[0][propertyName] ```

    js => 'Hatsune Miku'

    如您所见,在 JavaScript 中访问对象的属性值非常简单。 如果您已经知道值的名称,您可以使用点表示法。 在某些情况下,属性名是动态的或来自变量,您可以使用括号符号来访问该属性名的属性值。

在这个练习中,我们学习了用 JavaScript 创建对象的方法,以及如何修改和添加属性。 JavaScript 对象就像数组一样,很容易修改,而且不需要您指定模式。 在下一个活动中,我们将构建一个非常有趣的工具,它可以帮助你理解对象如何跨网络工作,以及如何有效地使用它们。

json

JSON.stringify是一个非常有用的工具,可以将 JavaScript 对象转换为格式化字符串。 稍后,字符串可以通过网络传输。

例如,我们有一个user对象,我们想把它转换成一个字符串:

const user = {
   name: 'r1cebank',
   favoriteFood: [
      'ramen',
      'sushi',
      'fried chicken'
   ]
};

如果我们想要将我们的对象转换为字符串,我们需要用这个对象调用JSON.stringify,如下面的代码所示:

JSON.stringify(user);

我们将得到这样的结果:

Figure 7.4: Result using JSON.stringify

图 7.4:使用 JSON.stringify 的结果

如您所见,调用JSON.stringify已经将我们的对象转换为对象的字符串表示形式。

但是由于它的执行方式,JSON.stringify是非常低效的。 尽管在大多数应用中性能差异并不明显,但在高性能应用中,一点点性能确实很重要。 一种快速JSON.stringify实用程序的方法是知道在最终输出中需要哪个属性。

练习 41:创建高效的 JSON。 Stringify

我们的目标是编写一个简单的函数,该函数接受一个对象和一系列属性,这些属性将包含在最终输出中。 然后该函数将调用JSON.stringify来创建对象的字符串版本。 让我们在Exercise41.js文件中定义一个函数betterStringify:

  1. 创建betterStringify功能:

    js function betterStringify(item, propertyMap) {

    js }

  2. 现在,我们将创建一个临时输出。 我们将存储我们想要包含在propertyMap中的属性:

  3. Iterate through our propertyMap argument to cherry-pick the property we want to include:

    js propertyMap.forEach((key) => {

    js });

    因为我们的propertyMap参数是一个数组,所以我们想使用forEach来遍历它。

  4. Assign the value from our item to the temporary output:

    js propertyMap.forEach((key) => {

    js if (item[key]) {

    js    output[key] = item[key];

    js }

    js });

    这里,我们正在检查是否设置了propertyMap参数中的键。 如果设置了该值,则将其存储在output属性中。

  5. Use a function on a test object:

    js const singer = {

    js name: 'Hatsune Miku',

    js age: 16,

    js birthday: 'August 31',

    js birthplace: 'Sapporo, Japan',

    js songList: [

    js   'World is mine',

    js   'Tell your world',

    js   'Melt'

    js ]

    js }

    js console.log(betterStringify(singer, ['name', 'birthday']))

    完成该函数后,运行该文件将产生以下输出:

Figure 7.5: Output of running better_stringify.js

图 7.5:运行 Exercise41.js 的输出

现在,是时候回答这个棘手的问题了:如果你做了这样的事情,你的代码能有多快?

如果您在JSON.stringify之上运行基准测试,您将看到 30%的性能提升:

Figure 7.6 Performance difference between JSON.stringify and ouR method

图 7.6 JSON 之间的性能差异 stringify 和 ouR 方法

你可以多花 30%的时间来计算更重要的东西。 请注意,这是一个非常简单的示例,说明如果您选择您的属性而不是使用JSON.stringify转储所有内容,您可以做什么。

数组和对象解构

在前面的练习和活动中,我们学习了修改对象和数组中的值的基本方法,以及从这些对象和数组中获取更多信息的方法。 还有一种方法可以使用析构赋值从数组或对象中检索值。

假设你已经得到了一个参数列表,你需要给变量赋值:

const param = ['My Name', 12, 'Developer'];

给它们赋值的一种方法是访问数组中的每一项:

const name = param[0];
const age = param[1];
const job = param[2];

我们还可以通过使用解构将其简化为一行:

[name, age, job] = param;

对数组使用析构赋值

在这个练习中,我们将声明一个名为userInfo的数组。 它将包括基本的用户信息。 我们还将声明几个变量,以便通过使用解构赋值将项目存储在数组中。 让我们开始:

  1. 创建userInfo数组:

    ```js

    const userInfo = ['John', 'chef', 34] ```

    js => undefined

  2. 创建用于存储nameagejob:

    ```js

    let name, age, job ```

    js => undefined

    的变量 3. Use the destructuring assignment syntax to assign values to our variables:

    ```js

    [name, job, age] = userInfo ```

    js => [ 'John', 'chef', 34 ]

    检查我们的价值观:

    ```js

    name ```

    js => 'John'

    ```js

    job ```

    js => 'chef'

    ```js

    age ```

    js => 34

  3. 你也可以使用以下代码忽略数组中的值:

当您处理的数据格式不完全按照您喜欢的方式时,解构赋值非常有用。 它还可以用于在数组中选择您想要的项。

练习 se 43:对一个对象使用解构赋值

在前面的练习中,我们声明了一个带有用户信息的数组,并使用解构赋值从数组中检索一些值。 对对象也可以做类似的事情。 在本练习中,我们将尝试在对象上解构赋值。 让我们开始:

  1. 创建一个名为userInfo的对象:

    ```js

    const userInfo = { name: 'John', job: 'chef', age: 34 } ```

    js => undefined

  2. 创建用于存储信息的变量:

  3. js > ({ name, job } = userInfo)

    js => { name: 'John', job: 'chef', age: 34 }

  4. Check the values:

    ```js

    name ```

    js => 'John'

    ```js

    job ```

    js => 'chef'

    请注意,在对对象使用解析赋值时,它就像一个过滤器,变量名必须匹配,您可以有选择地选择要选择的数组中的属性。 对于不需要预先声明变量的对象,还有一种不同的使用方法。

  5. js > userInfo = ['John', 'chef', 34]

    js => undefined

    ```js

    [ name, , age] = userInfo ```

    js => undefined

    ```js

    name ```

    js => 'John'

    ```js

    age ```

    js => 34

  6. Use the destructuring operator to create a variable from the object values:

    ```js

    const userInfoObj = { name: 'John', job: 'chef', age: 34 } ```

    js => undefined

    ```js

    let { job } = userInfoObj ```

    js => undefined

    ```js

    job ```

    js => 'chef'

    下面是上述代码的输出:

Figure 7.7: Output of the job variable

图 7.7:作业变量的输出

在本练习中,我们介绍了如何使用析构操作符从对象和数组中提取特定信息。 当我们处理大量信息时,这是非常有用的,而我们只想传递这些信息的一个子集。

传播运营商

在前面的练习中,我们学习了从对象或数组中获取特定信息的一些方法。 还有另一个操作符可以帮助我们展开数组或对象。 扩展操作符被添加到 ES6 规范中,但在 ES9 中,它还添加了对对象扩展的支持。 扩展运算符的功能是将每个项扩展为单独的项。 对于数组,当使用扩展运算符时,可以将其视为包含不同值的列表。 对于对象,它们将分散到键-值对中。 在下一个练习中,我们将探索在应用中使用传播运算符的不同方法。

要使用扩展运算符,我们在任何 iterrable 对象前面使用三个点(),如下所示:

printUser(...userInfo)

练习 e44:使用扩展运算符

在这个练习中,我们将看到传播算子如何帮助我们。 我们将使用前面练习中的原始userInfo数组。

执行以下步骤来完成练习:

  1. 创建userInfo数组:

    ```js

    const userInfo = ['John', 'chef', 34] ```

    js => undefined

  2. 创建一个打印用户信息的函数:

    ```js

    function printUser(name, job, age) { ```

    js ... console.log(name + ' is working as ' + job + ' and is ' + age + ' years old');

    js ... }

    js => undefined

  3. Spread the array into a list of arguments:

    ```js

    printUser(...userInfo) ```

    js John is working as chef and is 34 years old

    如您所见,在不使用扩展操作符的情况下调用该函数的原始方法是使用数组访问操作符,并对每个参数重复此操作。 因为数组的顺序与各自的参数相匹配,所以我们可以使用扩展运算符。

  4. 当你想要合并数组时,使用扩展运算符:

  5. Use the spread operator as a way to copy an array:

    ```js

    let detailedInfoCopy = [ ...detailedInfo ]; ```

    js => undefined

    ```js

    detailedInfoCopy ```

    js => [ 'male', 'John', 'chef', 34, 'July 5' ]

    在对象上使用扩展运算符是非常强大和实用的。

  6. 创建一个名为userRequest的新对象:

    ```js

    const userRequest = { name: 'username', type: 'update', data: 'newname'} ```

    js => undefined

  7. 使用object扩散克隆对象:

    ```js

    const newObj = { ...userRequest } ```

    js => undefined

    ```js

    newObj ```

    js => { name: 'username', type: 'update', data: 'newname' }

  8. js > const detailedRequestObj = { data: new Date(), new: true, ...userRequest}

    js => undefined

    ```js

    detailedRequestObj ```

    js => { data: 'newname', new: true, name: 'username', type: 'update' }

你可以看到,当你想复制所有的属性到一个新对象时,扩展运算符是非常有用的。 在许多应用中,您可以看到这一点,在这些应用中,您希望用一些通用属性包装用户请求以进行进一步处理。

休息操作

在前一节中,我们讨论了扩展操作符。 同样的操作符也可以以不同的方式使用。 在函数声明中,它们被称为剩余运算符

Rest 操作符主要用于表示数量不定的参数。 然后,参数将被放置在一个数组中:

function sum(...numbers) {
   console.log(numbers);
}
sum(1, 2, 3, 4, 5, 6, 7, 8, 9);

如你所见,我们在名字前用了同样的三个点。 这告诉我们的代码,我们期望这个函数的参数数目是无限的。 当我们调用带有参数列表的函数时,它们将被放入 JavaScript 数组中:

Figure 7.8: Output of sum when called with a list of numbers

图 7.8:使用数字列表调用 sum 时的输出

这并不意味着你不能控制争论的数量。 你可以像这样写函数声明,让 JavaScript 根据你的喜好映射几个参数,其余的映射到一个数组中:

function sum(initial, ...numbers) {
   console.log(initial, numbers);
}

这将第一个参数映射到变量 initial,其余的映射到一个名为numbers的数组:

sum(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

下面是上述代码的输出:

Figure 7.9: Output of sum when called with 0 and 1-9.

图 7.9:当调用 0 和 1-9 时 sum 的输出

JavaScript 的 OOP

由于 JavaScript 在 web 开发中的流行,它主要以功能的方式使用。 这导致许多开发人员认为没有办法用 JavaScript 实现 OOP。 甚至在 ES6 标准发布之前,就有一种定义类的方法:使用函数。 您以前可能在遗留前端代码中见过这种定义类的方法。 例如,如果你想创建一个名为Food的类,你必须这样写:

function Food(name) {
   this.name = name;
}
var leek = new Food("leek");
console.log(leek.name); // Outputs "leek"

在 ES6 发布之后,越来越多的开发人员采用了使用class关键字编写 JavaScript 类的现代方式。 在本章中,我们将讨论使用 ES6 标准声明类的方法。

用 JavaScript 定义类

在深入研究 JavaScript 中定义类的最新语法之前,让我们先回顾一下 ES6 之前是如何定义类的。

在 ES6 之前定义类的语法如下:

function ClassName(param1, param2) {
   // Constructor Logic
}

本质上,我们正在定义constructor类。 函数的名称将是类的名称。

用 ES6 定义类的语法如下:

class ClassName {
   constructor(param1, param2) {
      // Constructor logic
   }
   method1(param) {
      // Method logic
   }
}

在其他语言中,我们通常是这样处理类定义的。 在这里,我们可以定义构造函数和方法。

使用函数声明一个对象构造函数

在这个练习中,我们将创建一个非常简单的类Food。 稍后,我们还将向类添加一些方法。 我们将在这里使用函数构造函数方法。 让我们开始:

  1. 定义Food构造函数:

    js function Food(name, calories, cost) {

    js    this.name = name;

    js    this.calories = calories;

    js    this.cost = cost;

    js }

  2. 将该方法添加到构造函数中:

  3. 使用Food构造函数创建一个新对象:
  4. Call the method we have declared:

    js console.log(burger.description());

    下面是上述代码的输出:

Figure 7.10: Output of the burger.description() method

图 7.10:burger.description()方法的输出

很多人可能都熟悉类的这种声明类型。 但这也带来了问题。 首先,使用函数作为构造函数让开发人员不清楚何时将函数视为函数,何时将其用作构造函数。 后来,当 JavaScript 发布 ES6 时,它引入了一种声明类的新方法。 在下一个练习中,我们将使用新方法声明Food类。

练习 46:用 JavaScript 创建一个类

在本练习中,我们将用 JavaScript 创建一个类定义来存储食品数据。 它将包括名字、成本和卡路里含量。 稍后,我们还将创建返回食物描述的方法,以及输出特定食物的卡路里的另一个静态方法。 让我们开始:

  1. 宣布Food类:

    js class Food {

    js }

  2. Run typeof on the class name to see what type it is:

    js console.log(typeof Food) // should print out 'function'

    下面是上述代码的输出:

    Figure 7.11: Running the typeof command on the class

    图 7.11:在类上运行 typeof 命令

    正如你所看到的,我们刚刚声明的新类的类型是function——这不是很有趣吗? 这是因为,在 JavaScript 内部,我们声明的类只是编写constructor函数的另一种方式。

  3. Let's add our constructor:

    js class Food {

    js    constructor(name, calories, cost) {

    js       this.name = name;

    js       this.calories = calories;

    js       this.cost = cost;

    js    }

    js }

    与任何其他语言一样,类定义将包含一个构造函数,使用new关键字调用该构造函数来创建该类的实例。

  4. js class Food {

    js    constructor(name, calories, cost) {

    js       this.name = name;

    js       this.calories = calories;

    js       this.cost = cost;

    js    }

    js    description() {

    js       return this.name + ' calories: ' + this.calories;

    js    }

    js }

  5. If you try to invoke the Food class constructor like a function, it will throw the following error:

    js Food('burger', 1000, 9);

    js // TypeError: Class constructor Food2 cannot be invoked without 'new'

    下面是上述代码的输出:

    Figure 7.12: TypeError for invoking the constructor as a function

    图 7.12:将构造函数作为函数调用时的 TypeError

    请注意,当您试图像调用函数那样调用构造函数时,运行库会抛出一个错误。 这是非常有用的,因为它可以防止开发人员错误地将构造函数作为函数调用。

  6. 使用类构造函数创建一个新的食物对象:

    js let friedChicken = new Food('fried chicken', 520, 5);

  7. 调用我们声明的方法:

    js console.log(friedChicken.description());

  8. static声明方法,该方法返回的卡路里数量:

    js class Food {

    js    constructor(name, calories, cost) {

    js       this.name = name;

    【4】【5】

    js    }

    js    static getCalories(food) {

    【显示】

    js    }

    js    description() {

    js       return this.name + ' calories: ' + this.calories;

    【病人】

    js }

  9. Call the static method with the object we just created:

    js console.log(Food.getCalories(friedChicken)); /// 520

    下面是上述代码的输出:

Figure 7.13: Output generated after calling the static method of the Food class

图 7.13:调用 Food 类的静态方法后生成的输出

像任何其他编程语言一样,你可以调用static方法而不需要实例化对象。

现在我们已经了解了在 JavaScript 中声明类的新方法,让我们来谈谈类声明的一些区别:

  • 构造函数方法是必需的。 如果没有声明,JavaScript 将添加一个空构造函数。
  • 类声明不会被挂起,这意味着在声明之前不能使用它。 因此,最好将类定义或导入放在代码的顶部。

使用对象创建简单的用户信息缓存

在本节中,我们将设计一个简单的用户信息缓存。 缓存是一个临时位置,当从原始位置获取最频繁访问的项需要时间时,您可以在这里存储它们。 假设您正在为一个处理用户配置文件的后端应用设计。 每当请求传入时,服务器需要调用数据库来检索用户概要文件并将其发送回处理程序。 您可能知道,调用数据库是非常昂贵的操作。 作为后端开发人员,您可能会被要求提高服务的读取性能。

在下一个练习中,您将创建一个简单的缓存来存储用户配置文件,以便在大多数情况下可以跳过对数据库的请求。

Exercise 47: Creating a Cache Class to Add/Update/Remove Records from the Data Store .Exercise 47: Creating a Cache Class to Add/Update/Remove Records from the Data Store

在本练习中,我们将创建一个包含本地内存数据存储的缓存类。 它还包括从数据存储中添加/更新/删除记录的方法。

执行以下步骤来完成这个练习:

  1. Create the MySimpleCache class:

    js class MySimpleCache {

    js constructor() {

    js    // Declare your cache internal properties here

    js    this.cacheItems = {};

    js }

    js }

    在构造函数中,我们还将初始化缓存的内部状态。 这将是一个简单的对象。

  2. 定义addItem,它将设置键的缓存项:

    js addItem(key, value) {

    js // Add an item with the key

    js this.cacheItems[key] = value;

    js   }

  3. 定义updateItem,使用我们已经定义的addItem:

    js updateItem(key, value) {

    js // Update a value use the key

    js this.addItem(key, value);

    js }

  4. 定义removeItem。 这将删除存储在缓存中的对象,并调用之前创建的updateItem方法:

    js removeItem(key) {

    js this.updateItem(key, undefined);

    js }

  5. Test our cache using assert() with testMycache by updating and deleting a few users:

    js function testMyCache() {

    js    const cache = new MySimpleCache ();

    js    cache.addItem('user1', { name: 'user1', dob: 'Jan 1' });

    js    cache.addItem('user2', { name: 'user2', dob: 'Jul 21' });

    js    cache.updateItem('user1', { name: 'user1', dob: 'Jan 2' });

    js    cache.addItem('user3', { name: 'user3', dob: 'Feb 1' });

    js    cache.removeItem('user3');

    js    assert(cache.getItem('user1').dob === 'Jan 2');

    js    assert(cache.getItem('user2').dob === 'Jul 21');

    js    assert(cache.getItem('user3') === undefined);

    js    console.log ('=====TEST PASSED=====')

    js }

    js testMyCache();

    请注意

    assert()是一个内置的 Node.js 函数,它接受一个表达式。 如果表达式的值为true,它将传递;如果表达式的值为false,则抛出异常。

    运行该文件后,您应该没有看到错误和以下输出:

Figure 7.14: Output of simple_cache.js

图 7.14:simple_cache.js 的输出

类继承

到目前为止,我们只在 JavaScript 中创建了简单的类定义。 在 OOP 中,我们也可以让一个类继承另一个类。 类继承只是使一个类的实现派生自另一个类。 创建的子类将具有父类的所有属性和方法。 如下图所示:

Figure 7.15: Class inheritance

图 7.15:类继承

类继承提供了一些好处:

  • 它创建干净的、可测试的和可重用的代码。
  • 它减少了类似代码的数量。
  • 它减少了编写应用于所有子类的新特性时的维护时间。

在 JavaScript 中,创建继承自另一个类的子类非常容易。 为了做到这一点,使用extends关键字:

class MySubClass extends ParentClass {
}

实现一个子类

在这个练习中,我们将定义一个名为Vehicle的超类,并从它创建子类。 超类有startbuynamespeedcost等方法作为其属性。

超类的构造函数将接受名称、颜色和速度属性,然后将它们存储在对象中。

start方法将简单地打印出一个字符串,告诉您正在使用哪一辆车以及您正在如何旅行。 buy功能将打印出您将要购买的车辆。

执行以下步骤来完成这个练习:

  1. Vehicle类定义:

    js class Vehicle {

    js    constructor(name, speed, cost) {

    js       this.name = name;

    js       this.speed = speed;

    js       this.cost = cost;

    js    }

    js    start() {

    js       console.log('Starting vehicle, ' + this.name + ' at ' + this.speed + 'km/h');

    js    }

    js    buy() {

    js       console.log('Buying for ' + this.cost);

    js    }

    js }

  2. Create a vehicle instance and test out its methods:

    js const vehicle = new Vehicle('bicycle', 15, 100);

    js vehicle.start();

    js vehicle.buy();

    您应该看到以下输出:

    Figure 7.16: Output of the Vehicle class

    图 7.16:Vehicle 类的输出
  3. 创建CarPlaneRocket子类:

    js class Car extends Vehicle {}

    js class Plane extends Vehicle {}

    js class Rocket extends Vehicle {}

  4. CarPlaneRocket,覆盖start方法:

    js class Car extends Vehicle {

    【5】

    js       console.log('Driving car, at ' + this.speed + 'km/h');

    js    }

    【显示】

    js class Plane extends Vehicle {

    js    start() {

    js       console.log('Flying plane, at ' + this.speed + 'km/h');

    【病人】

    js }

    js class Rocket extends Vehicle {

    js    start() {

    【t16.1】

    js    }

    js }

  5. 创建PlaneRocketCar:

    js const car = new Car('Toyota Corolla', 120, 5000);

    js const plane = new Plane('Boeing 737', 1000, 26000000);

    js const rocket = new Rocket('Saturn V', 9920, 6000000000);

    的实例 6. Call the start method on all three objects:

    js car.start();

    js plane.start();

    js rocket.start();

    下面是上述代码的输出:

    Figure 7.17: Output from the objects

    图 7.17:对象的输出

    当您现在调用这些 start 方法时,您可以清楚地看到输出是不同的。 在声明子类时,大多数时候,我们需要覆盖来自父类的一些方法。 当我们在减少重复代码的同时保留由此创建的定制时,这是非常有用的。

    定制还不止于此——您还可以使用不同的构造函数创建一个新的子类。 您还可以从子类调用父方法。

  6. 我们先前创建的子类,我们将修改Car子类,它包括额外参数的构造函数:

    js class Car extends Vehicle {

    js    constructor(name, speed, cost, tankSize) {

    js       super(name, speed, cost);

    【4】【5】

    js    start() {

    js       console.log('Driving car, at ' + this.speed + 'km/h');

    【显示】

    js }

  7. Check to see whether the extra property is set:

    js const car2 = new Car('Toyota Corolla 2', 120, 5000, 2000);

    js console.log(car2.tankSize); // 2000

    下面是上述代码的输出:

Figure 7.18: Checking the extra property of the Car class

图 7.18:检查 Car 类的额外属性

正如你所看到的,声明一个子类是非常容易的——当你用这种方式编码时,你可以共享很多代码。 同时,你也不会失去进行定制的能力。 在 ES6 标准之后,你可以像其他 OOP 语言一样轻松地定义类。 它可以使您的代码更干净,更可测试,更易于维护。

私有和公有方法

在 OOP 中,有时将公开可访问的属性和函数与私有可访问的属性和函数分开是很有用的。 它是一个保护层,用于防止使用类的开发人员调用或访问类的某些内部状态。 在 JavaScript 中,这种行为是不可能的,因为 ES6 不允许声明私有属性; 您在类中声明的所有属性都将是可公开访问的。 为了实现这种类型的行为,一些开发人员选择使用下划线前缀,例如,privateMethod(),以通知其他开发人员不要使用它。 但是,也有一些关于声明私有方法的技巧。 在下一个练习中,我们将探讨私有方法。

Vehicle 类中的私有方法

在本练习中,我们将尝试为之前创建的Car类声明一个私有函数,以便在稍后将类导出为模块时不会暴露我们的私有方法。 让我们开始:

  1. 创建一个函数:printStat:

    js function printStat() {

    js    console.log('The car has a tanksize of ', this.tankSize);

    js }

  2. Modify the public method to use the function we just declared:

    js class Car extends Vehicle {

    js    constructor(name, speed, cost, tankSize) {

    js       super(name, speed, cost);

    js       this.tankSize = tankSize;

    js    }

    js    start() {

    js       console.log('Driving car, at ' + this.speed + 'km/h');

    js       printStat();

    js    }

    js }

    我们直接从我们的start方法调用printStat,但是不使用类中的一个方法就无法直接访问该方法。 通过在外部声明方法,我们使方法成为private

  3. Create another car instance and call the start method:

    js const car = new Car('Toyota Corolla', 120, 5000, 2000);

    js car.start();

    当你运行这段代码时,你会意识到这会导致异常:

    Figure 7.19: Output of printStat

    图 7.19:printStat 的输出
  4. Modify the start method so that the function knows about the object instance we are calling it from:

    js start() {

    js       console.log('Driving car, at ' + this.speed + 'km/h');

    js       printStat.bind(this)();

    js    }

    注意我们使用了.bind()。 通过使用 bind,我们将当前实例绑定到函数中的this变量。 这使得我们的代码按照预期工作:

Figure 7.20: Output of printStat after using .bind()

图 7.20:使用.bind()后的 printStat 输出

正如你所看到的,目前还没有办法在 JavaScript 中轻松地声明private方法或属性。 这个例子只是解决了这个问题; 它仍然没有像其他 OOP 语言(如 Java 或 Python)那样提供相同的分离。 在线上也有一些选项,可以使用符号声明私有方法,但如果您知道在哪里查找,也可以访问它们。

数组和对象内置方法

在前面,我们讨论了基本数组和对象。 它们处理我们如何存储数据。 现在,我们将深入研究如何获取存储在其中的数据,并对其进行高级计算和操作。

array.map(function)

数组映射将遍历数组中的每一项,并返回一个新数组作为结果。 传递给该方法的函数将接受当前项作为参数,函数的返回值将包含在 final 数组的结果中; 例如:

const singers = [{ name: 'Miku', age: 16}, { name: 'Kaito', age: 20 }];

如果我们想创建一个新数组,并且只包含列表中对象的 name 属性,我们可以使用array.map来实现:

const names = singers.map((singer) => singer.name);

下面是上述代码的输出:

Figure 7.21: Output using array.map(function)

图 7.21:使用 array.map 输出(函数)

array.forEach(function)

.forEach是一种遍历数组项的方法。 与.map不同,它不返回新值。 我们传入的函数被数组中的值反复调用; 例如:

const singers = [{ name: 'Miku', age: 16}, { name: 'Kaito', age: 20 }];
singers.forEach((singer) => {
   console.log(singer.name);
})

这将打印出数组中每个歌手的名字。

array.find(function)

.find方法与.map.forEach方法相似; 它接受一个函数作为参数。 这个函数将用于确定当前对象是否符合搜索的要求。 如果找到匹配,它将用作方法的返回结果。 此方法仅在数组中只有一个匹配项时有用,并且如果找到多个匹配项则根本不会返回。 例如,如果我们想要找到名称为字符串的对象,可以执行以下操作:

const singers = [{ name: 'Miku', age: 16}, { name: 'Kaito', age: 20 }];
const miku = singers.find((singer) => singer.name === 'Miku');

array.filter(function)

.filter的工作原理与.find类似,但它允许多个项目返回。 如果要匹配列表中的多个项,则需要使用.filter。 如果我们想找到年龄在 30 岁以下的歌手列表,请使用以下代码:

const singers = [{ name: 'Miku', age: 16}, { name: 'Kaito', age: 20 }];
const youngSingers = singers.filter((singer) => singer.age < 30);

数组中的map方法在遍历数组中的每一项时创建一个新数组。 map方法接受一个类似forEach方法的函数。 当它执行时,它将用第一个参数和当前项调用函数,用第二个参数和当前索引调用函数。 map方法还期望返回提供给它的函数。 返回值将被放入一个新数组中,并由方法返回,如下所示:

const programmingLanguages = ['C', 'Java', 'Python'];
const myMappedArray = programmingLanguages.map((language) => {
   return 'I know ' + language;
});

.map方法将遍历数组,map函数将返回"I know,"加上当前语言。 因此,myMappedArray的结果如下:

Figure 7.22: Example output using an array map method

图 7.22:使用数组映射方法的示例输出

我们将在第 10 章JavaScript中详细讲解array.map

我们将在下面的练习中使用的另一种方法是forEach方法。 forEach方法要干净得多,因为不需要管理当前索引和编写对函数的实际调用。 forEach方法是一个内置的数组方法,它接受一个函数作为参数。 使用forEach方法的示例如下:

foods.forEach(eat_food);

在下面的练习中,我们将对数组使用迭代方法。

在数组上使用迭代方法

有许多方法可以遍历数组。 一种是使用带有索引的for循环,另一种是使用它的内置方法之一。 在这个练习中,我们将初始化一个字符串数组,然后研究 JavaScript 中可用的一些迭代方法。 让我们开始:

  1. 创建一个食物列表作为数组:

    js const foods = ['sushi', 'tofu', 'fried chicken'];

  2. Join every item in the array using join:

    js foods.join(', ');

    下面是上述代码的输出:

    Figure 7.23: Joined items in the array

    图 7.23:数组中的连接项

    数组连接是遍历数组中的每个项的另一种方法,使用数组之间提供的分隔符将它们组合成单个字符串。

  3. 创建一个函数:eat_food:

    js function eat_food(food) {

    js    console.log('I am eating ' + food);

    js }

  4. Use the for loop to iterate through the array and call the function:

    js const foods = ['sushi', 'tofu', 'fried chicken'];

    js function eat_food(food) {

    js    console.log('I am eating ' + food);

    js }

    js for(let i = 0; i < foods.length; i++) {

    js    eat_food(foods[i]);

    js }

    下面是上述代码的输出:

    Figure 7.24: Output of eat_food being called inside a loop

    图 7.24:循环中调用 eat_food 的输出
  5. Use the forEach method to achieve the same:

    js foods.forEach(eat_food);

    下面是上述代码的输出:

    Figure 7.25: The same output is generated by using the forEach method

    图 7.25:使用 forEach 方法生成相同的输出

    因为eat_food是一个函数,它的第一个参数引用当前项,所以我们可以直接传递函数名。

  6. Create a new array of calorie numbers:

    js const nutrition = [100, 50, 400]

    这个数组包含了我们的food数组中每个项目的所有卡路里。 接下来,我们将使用一个不同的迭代函数来创建一个新的对象列表,包括这个信息。

  7. 创建新的对象数组:

    js const foodInfo = foods.map((food, index) => {

    js    return {

    js       name: food,

    js       calories: nutrition[index]

    js    };

    js });

  8. Print out foodInfo to the console:

    js console.log(foodInfo);

    下面是上述代码的输出:

Figure 7.26: Array containing food and calorie information

图 7.26:包含食物和卡路里信息的数组

运行array.map之后,将创建新的数组,其中包括我们的食品名称及其卡路里含量的信息。

在这个练习中,我们学习了两种迭代方法,forEachmap。 每个都有自己的功能和用法。 在大多数应用中,映射通常通过在每个数组项上运行相同的代码来计算数组结果。 如果您想在不直接修改数组的情况下操作数组中的每个项,这是非常有用的。

练习 51:查找和过滤数组

前面,我们讨论了遍历数组的方法。 这些方法也可以用于查找。 我们都知道,当您从头到尾迭代数组时,查找是非常昂贵的。 幸运的是,JavaScript 数组有一些用于此的内置方法,所以我们不必自己编写搜索函数。 在这个练习中,我们将使用includesfilter来搜索数组中的项。 让我们开始:

  1. profiles:

    js let profiles = [

    js    'Michael Scott',

    js    'Jim Halpert',

    js    'Dwight Shrute',

    js    'Random User',

    js    'Hatsune Miku',

    js    'Rin Kagamine'

    js ];

  2. Try to find out whether the list of profiles includes a person named Jim Halpert:

    js let hasJim = profiles.includes('Jim Halpert');

    js console.log(hasJim);

    下面是上述代码的输出:

    Figure 7.27: Output of the hasJim method

    图 7.27:hasJim 方法的输出
  3. Modify the profiles array to include extra information:

    js const profiles = [

    js    { name: 'Michael Scott', age: 42 },

    js    { name: 'Jim Halpert', age: 27},

    js    { name: 'Dwight Shrute', age: 37 },

    js    { name: 'Random User', age: 10 },

    js    { name: 'Hatsune Miku', age: 16 },

    js    { name: 'Rin Kagamine', age: 14 }

    js ]

    现在,数组不再是一个简单的字符串列表——它是一个对象列表,当我们处理对象时,事情会有点不同。

  4. Try to use includes to find the Jim Halpert profile again:

    js hasJim = profiles.includes({ name: 'Jim Halpert', age: 27});

    js console.log(hasJim);

    下面是上述代码的输出:

    Figure 7.28: Output of the hasJim method

    图 7.28:hasJim 方法的输出
  5. 查找名称为Jim Halpert:

    js hasJim = !!profiles.find((profile) => {

    js    return profile.name === 'Jim Halpert';

    js }).length;

    js console.log(hasJim);

    的配置文件 6. Find all the users with an age older than 18:

    js const adults = profiles.filter((profile) => {

    js    return profile.age > 18;

    js });

    js console.log(adults);

    当您运行上述代码时,它应该输出年龄超过 18 岁的所有用户。 filterfind的区别在于filter返回一个数组:

Figure 7.29: Output after using the filter method

图 7.29:使用 filter 方法后的输出

在这个练习中,我们研究了在数组中定位特定项的两种方法。 通过使用这些方法,我们可以避免重写搜索算法。 findfilter之间的区别是filter返回一个符合要求的所有对象的数组。 在实际的生产环境中,当我们想要测试的数组对象是否符合我们的要求,我们通常使用find方法因为它停止扫描当它找到一个匹配,而相比之下filter所有数组中的对象和将返回所有匹配的事件。 如果您只是测试某物的存在性,那么这种方法的成本会更高。 我们还使用了双负运算符将结果转换为布尔值。 如果您稍后要在条件语句中使用此值,那么这种表示法非常有用。

分类

排序是开发人员面临的最大挑战之一。 当我们想要对数组中的许多项进行排序时,通常需要定义一个特定的排序算法。 这些算法通常需要我们编写大量的排序逻辑,而且不容易重用。 在 JavaScript 中,我们可以使用内置数组方法来对自定义项列表进行排序,并编写最小的自定义代码。

在 JavaScript 数组中排序需要我们调用数组上的.sort()函数。 sort()函数有一个参数,称为排序比较器。 基于比较器,sort()函数将决定如何安排每个元素。

下面是我们将在接下来的练习中使用的一些其他函数的简要描述。

compareNumber函数只计算ab的差值。 在sort方法中,我们可以声明自己的自定义比较函数,以便传递下去进行比较:

function compareNumber(a, b) {
   return a - b;
}

compareAge功能与compareNumber功能非常相似。 这里唯一的区别是我们比较的是 JavaScript 对象而不是数字:

function compareAge(a, b) {
   return a.age - b.age;
}

使用 JavaScript 对数组进行排序

在这个练习中,我们将学习排序数组的方法。 在计算机科学中,排序总是很复杂。 在 JavaScript 中,数组对象内置了一个排序方法,可以对数组进行基本排序。

我们将使用前面练习中的profiles对象数组。 让我们开始:

  1. 创建一个数组numbers:

    js const numbers = [ 20, 1, 3, 55, 100, 2];

  2. Call array.sort() to sort this array:

    js numbers.sort();

    js console.log(numbers);

    当你运行前面的代码时,你会得到以下输出:

    Figure 7.30: Output of array.sort()

    图 7.30:array.sort()的输出

    这不是我们想要的; 看起来,sort函数只是随机排列这些值。 这背后的原因是,在 JavaScript 中,array.sort()并不真正支持按值排序。 默认情况下,它将所有内容都视为字符串。 当我们使用数字数组调用它时,它将所有内容转换成字符串,然后开始排序。 这就是为什么数字 1 出现在 2 和 3 之前。 为了实现数字的排序,我们需要做一些额外的事情。

  3. Define the compareNumber function:

    js function compareNumber(a, b) {

    js    return a - b;

    js }

    函数期望接受两个将要进行比较的值,并返回一个必须匹配以下条件的值:如果a小于b,返回一个小于 0 的数字; 如果a等于b,返回 0; 如果a大于b,则返回一个大于 0 的数字。

  4. Run the sort function and provide the compareNumber function as our parameter:

    js numbers.sort(compareNumber);

    js console.log(numbers);

    当你运行前面的代码时,你会看到这个函数已经按照我们想要的顺序对数组进行了排序:

    Figure 7.31: Output of array.sort(compareNumber)

    图 7.31:array.sort(compareNumber)的输出

    现在,数组按照从最小到最大的顺序正确排序。 然而,大多数时候当我们必须进行排序时,我们需要对复杂对象进行排序。 对于下一步,我们将使用在前面练习中创建的profiles数组。

  5. js const profiles = [

    js    { name: 'Michael Scott', age: 42 },

    js    { name: 'Jim Halpert', age: 27},

    js    { name: 'Dwight Shrute', age: 37 },

    js    { name: 'Random User', age: 10 },

    js    { name: 'Hatsune Miku', age: 16 },

    js    { name: 'Rin Kagamine', age: 14 }

    js ]

  6. Call profiles.sort():

    js profiles.sort();

    js console.log(profiles);

    下面是上述代码的输出:

    Figure 7.32: Output of the profiles.sort() function

    图 7.32:profiles.sort()函数的输出

    因为我们的sort函数不知道如何比较这些对象,所以数组保持原样。 为了使它正确地排序对象,我们需要一个比较函数,就像上次一样。

  7. Define compareAge:

    js function compareAge(a, b) {

    js    return a.age - b.age;

    js }

    提供给compareAgeab的两个参数是数组中的对象。 因此,为了使它们正确排序,我们需要访问这些对象的age属性并对它们进行比较。

  8. Call the sort function with the compare function we just have defined:

    js profiles.sort(compareAge);

    js console.log(profiles);

    下面是上述代码的输出:

    Figure 7.33: Result of profile.sort(compareAge)

图 7.33:profile.sort 的结果(比较年龄)

在这个练习中,我们学习了数组排序的方法。 要记住的一件事是,在 JavaScript 中,如果不对字符串值进行排序,则需要向排序函数提供一个比较函数,以便它知道如何排序。 这种方法的空间和时间复杂度因平台而异,但如果你使用 Node.js, JavaScript 的 V8 引擎对这些类型的操作进行了高度优化,所以你不必担心性能。 在下一个练习中,我们将学习 JavaScript 中的数组操作——数组减速器,这个操作非常有趣,但也很有用。 通过使用数组减数器,我们可以轻松地组合数组中的项,并将它们减少为一个值。

Array Reduce

在构建后端应用时,很多时候会给出格式化的结果列表,您必须从中计算单个值。 虽然这可以使用传统的循环方法来完成,但当您使用 JavaScript 缩减函数时,它会更清晰,更容易维护。 减少意味着接受数组中的每个元素并产生一个返回值。

如果我们想减少一个数组,我们可以调用内置的array.reduce()方法:

Array.reduce((previousValue, currentValue) => {
   // reducer
}, initialValue);

当我们调用array.reduce()时,我们需要传入一个函数和初始值。 该函数将提供一个以前的值和一个当前值作为参数,并使用返回值作为最终值。

练习 53:使用 JavaScript Reduce 方法计算购物车

在本练习中,我们将尝试使用 JavaScriptreduce方法来计算购物车。 让我们开始:

  1. 创建购物车变量:

    js const cart = [];

  2. Push items into array:

    js cart.push({ name: 'CD', price: 12.00, amount: 2 });

    js cart.push({ name: 'Book', price: 45.90, amount: 1 });

    js cart.push({ name: 'Headphones', price: 5.99, amount: 3 });

    js cart.push({ name: 'Coffee', price: 12.00, amount: 2 });

    js cart.push({ name: 'Mug', price: 15.45, amount: 1 });

    js cart.push({ name: 'Sugar', price: 5.00, amount: 1 });

  3. Calculate the total cost of the shopping cart using the loop method:

    js let total = 0;

    js cart.forEach((item) => {

    js    total += item.price * item.amount;

    js });

    js console.log('Total amount: ' + total);

    下面是上述代码的输出:

    Figure 7.34: Result of the loop method of calculating total

    图 7.34:计算 total 的循环方法结果
  4. 我们写减速器为priceReducer:

    js function priceReducer (accumulator, currentValue) {

    js    return accumulator += currentValue.price * currentValue.amount;

    js }

  5. Call cart.reduce with our reducer:

    js total = cart.reduce(priceReducer, 0);

    js console.log('Total amount: ' + total);

    下面是上述代码的输出:

Figure 7.35: Result of cart.reduce

图 7.35:cart.reduce 的结果

在这个练习中,我们学习了在 JavaScript 中将数组缩减为单个值的方法。 虽然使用循环遍历数组并返回累加器是完全正确的,但当您使用 reduce 函数时,它会使代码更加清晰。 我们不仅减少了作用域内可变变量的数量,而且还使代码更加清晰和可维护。 下一个维护代码的人将知道该函数的返回值将是一个单独的值,而forEach方法可能会使返回的结果不清楚。

活动 9:使用 JavaScript 数组和类创建学生管理器

假设您正在为当地的一个学区工作,到目前为止,他们一直使用纸质注册表来跟踪学生信息。 现在,他们有了一些资金,想让你开发一个计算机软件来跟踪学生信息。 他们对软件有以下要求:

  • 它需要能够记录学生的信息,包括他们的名字、年龄、年级水平和书籍信息。
  • 每个学生将被分配一个唯一的 ID,用于检索和修改学生记录。
  • 图书信息将包括该学生的图书的名称和当前年级(编号年级)。
  • 需要有一种方法来计算学生的平均成绩。
  • 需要有一种方法来搜索所有年龄或年级相同的学生。
  • There needs to be a way to search for a student using their name. When multiples are found, return all of them.

    请注意

    这个活动的完整代码也可以在我们的 GitHub 存储库中找到,这里:https://github.com/TrainingByPackt/Professional-JavaScript/blob/master/Lesson07/Activity09/Activity09.js

执行以下步骤来完成此活动:

  1. 创建一个School类并在构造函数中初始化一个学生列表。
  2. 创建一个Student类,并存储课程列表,学生的agenamegrade level在其中。
  3. 创建一个包含coursenamegrades信息的Course类。
  4. School类中创建addStudent函数,将学生推入school对象中的列表中。
  5. School类中创建findByGrade函数,该函数返回具有给定grade level的所有学生。
  6. School类中创建findByAge函数,该函数返回具有相同age的学生列表。
  7. School课堂中创建findByName功能,按名字搜索学校里所有的学生。
  8. Student课堂上,创建一个calculateAverageGrade方法来计算学生的平均成绩。
  9. In the Student class, create a assignGrade method, which will assign a number grade for a course the student is taking.

    请注意

    这个活动的解决方案可以在 608 页找到。

在前一节中,我们讨论了允许迭代、查找和减少数组的方法。 这些是处理数组时非常有用的方法。 虽然大多数方法只完成基本任务,并且可以使用循环轻松实现,但使用它们有助于使我们的代码更可用和可测试。 运行时引擎也对一些内置方法进行了很好的优化。

在下一节中,我们将介绍 Map 和 Set 的一些内置函数。 如果我们需要在应用中跟踪值,它们非常有用。

地图和集合

在 JavaScript 中,映射和集是非常被低估的类型,但它们在某些应用中可能非常强大。 映射的工作原理与 JavaScript 中的基本 hashmap 类似,在需要跟踪键值对列表时非常有用。 当您需要保存一个唯一值列表时,可以使用集合。 大多数开发人员经常使用对象来做任何事情,但却忘记了在某些情况下,使用 map 和 set 会更有效率。 在下一节中,我们将学习地图和集合以及如何使用它们。

在很多情况下,我们必须在应用中跟踪唯一键值对的列表。 当使用其他语言编程时,我们经常需要实现一个叫做Hashmap的类。 在 JavaScript 中,有两种类型可以完成此任务:一种是 Map,另一种是 Object。 因为他们似乎在做同样的事情,许多 JavaScript 开发人员倾向于使用 Object 来解决所有问题,而忽略了使用 Map 有时对他们的用例更有效。

使用地图和对象

在这个练习中,我们将回顾使用 Maps 的方法,以及它们与对象的不同之处:

  1. 创建一个新的地图map:

    js const map = new Map()

  2. js const key1 = 'key1';

    js const key2 = { name: 'John', age: 18 };

    js const key3 = Map;

  3. Use map.set to set a value for all the keys we defined earlier:

    js map.set(key1, 'value for key1');

    js map.set(key2, 'value for key2');

    js map.set(key3, 'value for key3');

    下面是上述代码的输出:

    Figure 7.36: Output after assigning values to map.set

    图 7.36:给 map.set 赋值后的输出
  4. Get the values of the keys:

    js console.log(map.get(key1));

    js console.log(map.get(key2));

    js console.log(map.get(key3));

    下面是上述代码的输出:

    Figure 7.37: Output of console.log for value retrieval

    图 7.37:用于值检索的 console.log 的输出
  5. Retrieve the value for key2 without using the reference:

    js console.log(map.get({ name: 'John', age: 18 }));

    下面是上述代码的输出:

    Figure 7.38: Output of console.log when using get without reference

    图 7.38:在没有引用的情况下使用 get 时 console.log 的输出

    虽然我们正确地输入了所有内容,但 Map 似乎无法找到该键的值。 这是因为,在进行这些检索时,它使用的是对对象的引用,而不是值。

  6. Iterate through the Map using forEach:

    js map.forEach((value, key) => {

    js    console.log('the value for key: ' + key + ' is ' + value);

    js });

    Map 可以像数组一样进行迭代。 当使用forEach方法时,传入的函数将使用两个参数来调用:第一个参数是值,第二个参数是键。

  7. Get the list of keys and values as arrays:

    js console.log(map.keys());

    js console.log(map.values());

    下面是上述代码的输出:

    Figure 7.39: List of keys and values as arrays

    图 7.39:键和值的数组列表

    当您只需要它所存储的部分信息时,这些方法非常有用。 如果你有一个 Map 跟踪用户,同时使用他们的 id 作为键,调用values方法将简单地返回一个用户列表。

  8. Check whether the Map includes a key:

    js console.log(map.has('non exist')); // false

    下面是上述代码的输出:

Figure 7.40: Output indicating that Map does not include a key

图 7.40:表示 Map 不包含键的输出

请注意

在这里,我们可以看到 map 和 Objects 之间的第一个主要区别,尽管它们都能够跟踪唯一键值对的列表。 在 map 中,可以使用引用对象或函数的键。 这在 JavaScript 中的对象中是不可能的。 我们可以看到的另一件事是,它还根据键添加到 Map 的顺序保留键的顺序。 虽然在 Object 中可能会得到有序的键,但 JavaScript 并不保证键的顺序与它们被添加到 Object 中的顺序一致。

通过这个练习,我们了解了 Maps 的用法及其与 Object 的区别。 当你在处理键值数据时,你需要排序,Map 应该总是优先于 Objects,因为它不仅保持键的顺序,它还允许对象引用被用作键。 这是两种类型的主要区别。 在下一个练习中,我们将讨论另一种经常被开发人员忽略的类型:Set。

在数学中,集合被定义为不同对象的集合。 在 JavaScript 中,它很少被使用,但我们还是要讨论 Set 的一种用法。

练习 55:Using Sets to Track Unique Values(使用集合跟踪唯一值

在这个练习中,我们将学习 JavaScript Set。 我们将构建一个算法来删除数组中所有重复的值。

执行以下步骤来完成这个练习:

  1. planets:

    js const planets = [

    js    'Mercury',

    js    'Uranus',

    js    'Mars',

    js    'Venus',

    js    'Neptune',

    js    'Saturn',

    js    'Mars',

    js    'Jupiter',

    js    'Earth',

    js    'Saturn'

    js ]

  2. 使用数组

    js const planetSet = new Set(planets);

    创建一个新的 Set: 3. Retrieve the unique values in the planets array:

    js console.log(planetSet.values());

    下面是上述代码的输出:

    Figure 7.41: Unique array values

    图 7.41:唯一的数组值
  3. Add more values to the Set using the add method:

    js planetSet.add('Venus');

    js planetSet.add('Kepler-440b');

    我们可以使用add方法为 Set 添加一个新值,但由于 Set 总是保持其成员的唯一性,如果添加任何已经存在的值,它将被忽略:

    Figure 7.42: Failure to add duplicate values

    图 7.42:添加重复值失败
  4. 使用.size属性获取 Set 的大小:

    js console.log(planetSet.size);

  5. Clear all the values inside the Set:

    js planetSet.clear();

    js console.log(planetSet);

    下面是上述代码的输出:

Figure 7.43: All values cleared from the set

图 7.43:从集合中清除所有值

在这个练习中,我们介绍了使用 Set 作为工具来帮助删除数组中的重复值的一些方法。 当您希望以尽可能小的努力保存唯一值列表,而又不需要通过索引访问它们时,集合是非常有用的。 否则,如果您要处理许多可能包含重复项的项,数组仍然是最好的选择。 在下一节中,我们将讨论 Math、Date 和 String 方法。

数学,日期和字符串

在使用 JavaScript 构建复杂应用时,有时需要处理字符串操作、数学计算和日期。 幸运的是,JavaScript 有几个用于这种类型数据的内置方法。 在下面的练习中,我们将介绍在应用中使用这些功能的方法。

创建一个new Date对象,使用以下命令:

const currentDate = new Date();

这将指向当前日期。

要创建一个新字符串,使用以下命令:

const myString = 'this is a string';

要使用Math模块,我们可以使用Math类:

const random = Math.random();

练习 56:使用字符串方法

在本练习中,我们将介绍一些在应用中更容易处理字符串的方法。 字符串操作和构建在其他语言中一直是复杂的任务。 在 JavaScript 中,通过使用 String 方法,我们可以轻松地创建、匹配和操作字符串。 在本练习中,我们将创建各种字符串并使用 String 方法来操作它们。

执行以下步骤来完成这个练习:

  1. 创建一个名为planet的变量:

    js let planet = 'Earth';

  2. Create a sentence using template strings:

    js let sentence = `We are on the planet ${planet}`;

    模板字符串是 ES6 中引入的一个非常有用的特性。 我们可以通过组合模板和变量来创建字符串,而不需要创建字符串构建或使用字符串连接。 字符串模板使用``js 包装,而要插入字符串中的变量使用${}`包装。

  3. Separate our sentence into words:

    console.log(sentence.split(' '));js

    我们可以使用split方法和分隔符将字符串分割成数组。 在前面的例子中,JavaScript 将把我们的句子分成一个单词数组,就像这样:

    Figure 7.44: Splitting a string into an array of words

    图 7.44:将字符串分割为单词数组
  4. We can also use replace to replace any matched substring with another substring, as follows:

    sentence = sentence.replace('Earth', 'Venus');js

    console.log(sentence);js

    下面是上述代码的输出:

    Figure 7.45: Replacing a word in a string

    图 7.45:替换字符串中的单词

    replace方法中,我们将提供第一个参数作为在字符串中匹配的子字符串。 第二个参数是您想要替换的字符串。

  5. Check whether our sentence includes the word Mars:

    console.log(sentence.includes('Mars'));js

    下面是上述代码的输出:

    Figure 7.46: Checking the string for the presence of a character

    图 7.46:检查字符串中是否存在字符
  6. 您还可以将整个字符串转换为大写或小写:

  7. Get a character at index in the string using charAt:

    sentence.charAt(0); // returns Wjs

    因为句子不一定是数组,所以不能在索引(如数组)上访问特定字符。 为此,您需要调用charAt方法。

  8. Get the length of the string using the length property of the string:

    sentence.length;js

    下面是上述代码的输出:

Figure 7.47: Length of the sentence after our modification

图 7.47:我们修改后的句子长度

在这个练习中,我们学习了使用模板字符串和帮助我们操作字符串的字符串方法来构造字符串的方法。 这些在处理大量用户输入的应用中非常有用。 在下一个练习中,我们将学习 Math 和 Date 方法。

数学和日期

在这一节中,我们将学习数学和日期类型。 我们很少在应用中处理 Math,但当我们这样做时,利用 Math 库是非常有用的。 稍后,我们将讨论 Date 对象及其方法。 Math 和 Date 类包括各种有用的方法来帮助我们进行数学计算和日期操作。

练习 57:使用数学和日期

在这个练习中,我们将学习如何在 JavaScript 中实现 Math 和 Date 类型。 我们将使用它们来生成随机数,并使用它们的内置常数进行数学计算。 我们还将使用 Date 对象来测试在 JavaScript 中处理日期的不同方法。 让我们开始:

  1. 创建一个函数:generateRandomString:

    function generateRandomString(length) {js

    }js

  2. Create a function that generates a random number within a certain range:

    function generateRandomNumber(min, max) {js

    return Math.floor(Math.random() * (max - min + 1)) + min;js

    }js

    在上述函数中,Math.random生成一个介于 0(含)和 1(不含)之间的随机数。 当我们想要一个在这两个范围之间的数字时,我们也可以使用Math.floor来四舍五入,以确保输出中不包含max

  3. Use the random number generator function in generateRandomString:

    function generateRandomString(length) {js

    const characters = [];js

    const characterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';js

    for (let i = 0; i < length; i++) {js

    characters.push(characterSet.charAt(generateRandomNumber(0, characterSet.length)));js

    }js

    return characters.join(');js

    }js

    生成随机数所需的方法非常简单——我们有一个希望包含在随机字符串中的字符集。 稍后,我们将使用我们创建的函数运行一个循环来获得一个随机字符,使用charAt并将随机索引传递给它。

  4. Test out our function:

    console.log(generateRandomString(16));js

    下面是上述代码的输出:

    Figure 7.48: Output of our random String function

    图 7.48:随机 String 函数的输出

    每次我们运行这个函数,它都会给我们一个完全随机的字符串,其大小与我们刚才传递的字符串相同。 这是生成随机用户名的一种非常简单的方法,但不太适合生成 id,因为它并不能真正保证惟一性。

  5. Use Math constants to create a function that calculates circle areas, as follows:

    function circleArea(radius) {js

    return Math.pow(radius, 2) * Math.PI;js

    }js

    在这个函数中,我们使用了来自Math对象的Math.PI。 它被赋给实际的PI值的近似值。 我们还使用了Math.pow方法将半径从参数的平方提高到 2。 接下来,我们将探索 JavaScript 中的Date类型。

  6. Create a new Date object:

    const now = new Date();js

    console.log(now);js

    下面是上述代码的输出:

    Figure 7.49: Output of the new Date object

    图 7.49:新的 Date 对象的输出

    当我们不带任何东西创建新的Date对象时,它将生成一个存储当前时间的对象。

  7. Create a new Date object at a specific date and time:

    const past = new Date('August 31, 2007 00:00:00');js

    Date构造函数将接受一个可以被解析为日期的字符串参数。 当我们使用这个字符串调用构造函数时,它将在那个日期和时间上创建一个Date对象。

  8. Get the year, month, and date from our past Date object:

    console.log(past.getFullYear());js

    console.log(past.getMonth());js

    console.log(past.getDate());js

    下面是上述代码的输出:

    Figure 7.50: Year, month, and date of the past date object

    图 7.50:过去日期对象的年、月和日期

    返回的月份不是从 1 开始,1 月是 1。 相反,它是从 0 开始的,所以 8 月是 7。

  9. You can also generate a string represented version of the object by calling toString:

    console.log(past.toString());js

    下面是上述代码的输出:

    Figure 7.51: Date presented in string form

    图 7.51:以字符串形式显示的日期

    通过使用toString方法,我们可以简单地使用它在应用中保存时间戳记录。

  10. If you want to get the Unix time, you can use Date.now:

    console.log(Math.floor(Date.now() / 1000));js

    我们再次使用Math.floor的原因是我们需要将Date.now的输出除以 1000,因为它返回的单位是毫秒。

在这个练习中,我们介绍了几种在应用中使用 Math 和 Date 类型的方法。 当我们想要生成伪随机 id 或随机字符串时,它们非常有用。 当我们需要在应用中跟踪时间戳时,也会使用Date 对象。 在下一节中,我们将简要介绍符号、迭代器、生成器和代理。

符号,迭代器,生成器和代理

在 JavaScript 开发中,很少使用这些类型,但对于某些用例,它们可能非常有用。 在本节中,我们将介绍这些是什么,以及如何在应用中使用它们。

符号

符号是唯一的值; 它们可以用作标识符,因为每次调用Symbol()时,它都会返回一个唯一的符号。 甚至函数也返回 Symbol 类型。 但是,不能使用new关键字调用它,因为它不是构造函数。 当存储在对象中时,当你遍历属性列表时,它们不会被包含,所以如果你想将任何东西存储为对象中的属性,并且不想在运行JSON.stringify时暴露它们,你可以使用 Symbols 来实现这一点。

迭代器和生成器

迭代器和生成器经常一起使用。 生成器函数是其代码在被调用时不会立即执行的函数。 当一个值要从生成器返回时,需要使用yield调用它。 在此之后,它停止执行,直到再次调用下一个函数。 这使得生成器非常适合使用迭代器。 在迭代器中,需要定义具有next方法的函数,并且每次调用该函数时都会返回一个值。 通过同时使用这两者,我们可以使用大量可重用代码构建非常强大的迭代器。

符号在 JavaScript 中是一个很难理解的概念,而且它们并不常用。 在这个练习中,我们将复习几种使用符号的方法,并探索它们的属性。

练习 58:使用符号并探索其属性

在本练习中,我们将使用符号及其属性来识别对象属性。 让我们开始:

  1. 创建两个符号:

    let symbol1 = Symbol();js

    let symbol2 = Symbol('symbol');js

  2. Test their equivalence:

    console.log(symbol1 === symbol2);js

    console.log(symbol1 === Symbol('symbol'));js

    两个语句都将被计算为 false。 这是因为符号在 JavaScript 中是唯一的,即使它们有相同的名称,它们仍然不相等。

  3. const testObj = {};js

    testObj.name = 'test object';js

    testObj.included = 'this will be included';js

  4. 使用符号作为键在对象中创建一个属性:

  5. Print out the keys in the object:

    console.log(Object.keys(testObj));js

    下面是上述代码的输出:

    Figure 7.52: List of keys printed out using Object.keys

    图 7.52:使用 Object.keys 打印出的键的列表

    似乎调用Object.keys没有返回Symbol属性。 这背后的原因是,因为符号不是可枚举的,所以它们不会被Object.keysObject.getOwnPropertyNames返回。

  6. 让我们试着得到我们的Symbol属性的值:

  7. Use the Symbol registry:

    const anotherSymbolKey = Symbol.for('key');js

    const copyOfAnotherSymbol = Symbol.for('key');js

    在这个例子中,我们可以对Symbol键进行搜索,并将该引用存储在新的常量中。 Symbol注册表是应用中所有符号的注册表。 在这里,您可以将创建的符号存储在全局注册表中,以便稍后可以检索它们。

  8. Retrieve the content of the Symbol property using its reference:

    testObj[anotherSymbolKey] = 'another key';js

    console.log(testObj[copyOfAnotherSymbol]);js

    下面是上述代码的输出:

Figure 7.53: Result when we retrieve values using a symbol reference

图 7.53:使用符号引用检索值时的结果

当我们运行它时,它将打印出我们想要的结果。 当我们使用Symbol.for创建一个符号时,我们将在键和引用之间创建一个一对一的关系,这样当我们使用Symbol.for获得另一个引用时,这两个符号将是相等的。

在这个练习中,我们复习了符号的一些性质。 如果您需要将它们用作object属性的标识符,那么它们非常有用。 使用Symbol注册表还可以帮助我们重新定位之前创建的Symbol。 在下一个练习中,我们将讨论迭代器和生成器的一般用法。

在之前的练习中,我们学习了符号。 JavaScript 中还有另一种类型的Symbol,称为Symbol.iterator,这是一种用于创建迭代器的特殊符号。 在本练习中,我们将使用生成器创建一个可迭代对象。

迭代器和生成器

Python 中有一个非常有用的函数,叫做range(),它生成给定范围内的数字; 现在,让我们尝试用迭代器重新创建它:

  1. 创建一个名为range的函数,该函数返回一个带有iterator属性的对象:
  2. Use the for..in loop on our range function:

    for (let value of range(10)) {js

    console.log(value);js

    }js

    下面是上述代码的输出:

    Figure 7.54: Output using a for..in loop

    图 7.54:使用 for..in 循环的输出

    当我们运行它时,它只产生一个值。 为了修改它以产生多个结果,我们将用一个循环来包装它。

  3. Let's wrap the yield statement with a loop:

    function range(max) {js

    return {js

    *[Symbol.iterator]() {js

    for (let i = 0; i < max; i++) {js

    yield i;js

    }js

    }js

    };js

    }js

    正常情况下,这对returns不起作用,因为它只能返回一次。 这是因为生成器函数预计将使用.next()多次使用。 我们可以延迟它的执行,直到再次调用它:

    Figure 7.55: Output after wrapping the yield statement with a loop

    图 7.55:用循环包装 yield 语句后的输出

    为了更好地理解生成器函数,还可以定义一个简单的生成器函数,而不需要在迭代器中实现它。

  4. Create a generator function called gen:

    function* gen() {js

    yield 1;js

    }js

    这是生成器函数的一个非常简单的定义。 当调用它时,它将返回一个只能迭代一次的生成器。 但是,您可以使用前面的函数生成任意数量的生成器。

  5. 生成generator函数:

    const generator = gen();js

  6. Call the generator's next method to get its values:

    console.log(generator.next());js

    console.log(generator.next());js

    console.log(generator.next());js

    当我们在生成器上调用.next()时,它将执行我们的代码,直到它到达yield关键字。 然后,它将返回该语句生成的值。 它还包括一个done属性,用于指示该生成器是否已完成遍历所有可能的值。 一旦生成器达到done状态,就没有办法重新开始迭代,除非你正在修改内部状态:

Figure 7.56: Value after yielding the statement

图 7.56:生成语句后的值

如您所见,第一次调用 next 方法时,我们将得到值 1。 之后,done属性将被设置为true。 不管我们调用它多少次,它总是返回undefined,这意味着生成器已经完成了迭代。

在这个练习中,我们学习了迭代器和生成器。 它们在 JavaScript 中非常强大,在官方支持之前,许多早期的异步/等待功能都是使用生成器函数创建的。 下次创建可迭代的自定义类或对象时,可以创建生成器。 这使得代码更加清晰,因为不需要管理大量的内部状态。

代理

当需要对需要管理每个基本操作的对象进行额外的细粒度控制时,可以使用代理。 可以将 JavaScript 代理视为操作和对象之间的中间人。 每个对象操作都可以通过它拥有代理,这意味着您可以实现非常复杂的对象。 在下一个练习中,我们将介绍使用代理启用对象的创造性方法。

代理充当对象和程序其余部分之间的中间人。 对该对象所做的任何更改都将由代理转发,代理将决定如何处理该更改。

创建代理是非常简单的-所有你需要做的是用对象调用Proxy构造函数,包括我们的处理程序和我们代理的对象。 一旦创建了代理,您就可以将代理视为原始值,并且可以开始修改代理上的属性。

代理的使用示例如下:

const handlers = {
   set: (object, prop, value) => {
      console.log('setting ' + prop);
   }
}
const proxiesValue = new Proxy({}, handlers);
proxiesValue.prop1 = 'hi';
```js

我们创建了一个`proxiesValue`,并给它一个 set 处理器。 当我们尝试设置`prop1`属性时,我们将得到以下输出:

![Figure 7.57: Proxy value created ](img/C14587_07_57.jpg)

###### 图 7.57:创建的代理值

### 练习 60:使用代理构建复杂对象

在本练习中,我们将使用代理向您展示如何构建一个能够隐藏其值并在属性上强制执行数据类型的对象。 我们也将扩展和定制一些基本操作。 让我们开始:

1.  创建一个基本的 JavaScript 对象:
2.  创建一个`handlers`对象:

    ```
    const handlers = {
    ```js

    ```
    }
    ```js

3.  为基本对象创建一个代理包装:

    ```
    const proxiesValue = new Proxy(simpleObject, handlers);
    ```js

4.  Now, add `handlers` to our proxy:

    ```
    const handlers = {
    ```js

    ```
       get: (object, prop) => {
    ```js

    ```
          return 'values are private';
    ```js

    ```
       }
    ```js

    ```
    }
    ```js

    在这里,我们为我们的对象添加了一个`get`处理程序,我们忽略它请求的键,只是返回一个固定的字符串。 当我们这样做时,无论我们做什么,对象将只返回我们定义的值。

5.  Let's test our handler in the proxy:

    ```
    proxiedValue.key1 = 'value1';
    ```js

    ```
    console.log(proxiedValue.key1);
    ```js

    ```
    console.log(proxiedValue.keyDoesntExist);
    ```js

    下面是上述代码的输出:

    ![Figure 7.58: Testing the handler in the proxy ](img/C14587_07_58.jpg)

    ###### 图 7.58:在代理中测试处理程序

    当我们运行这段代码时,我们在对象中给`key1`赋了一个值,但是由于我们在试图读回值时定义的处理程序的方式,它总是给我们之前定义的字符串。 当我们在一个不存在的值上进行尝试时,它也会返回相同的结果。

6.  Let's add a `set` handler for validation:

    ```
    set: (object, prop, value) => {
    ```js

    ```
          if (prop === 'id') {
    ```js

    ```
            if (!Number.isInteger(value)) {
    ```js

    ```
               throw new TypeError('The id needs to be an integer');
    ```js

    ```
            }
    ```js

    ```
          }
    ```js

    ```
       }
    ```js

    我们添加了一个`set`处理程序; 每当我们试图对代理整数执行 set 操作时,这个处理程序都会被调用。

7.  尝试将`id`设置为字符串:

    ```
    proxiedValue.id = 'not an id'
    ```js

![Figure 7.59: Screenshot showing TypeError when trying to set id to string ](img/C14587_07_59.jpg)

###### 图 7.59:尝试设置 id 为字符串时显示 TypeError 的屏幕截图

正如您可能已经猜到的,当我们尝试设置这个值时,它会给我们一个`TypeError`异常。 如果您正在构建一个库,并且不希望内部属性被覆盖,那么这将非常有用。 您可以使用符号来实现这一点,但是使用代理也是一种选择。 它的另一个用途是实现验证。

在这个练习中,我们讨论了一些我们可以用来制作对象的创造性方法。 通过使用代理,我们可以创建具有内置验证的非常复杂的对象。

### JavaScript 重构

在大规模应用中使用 JavaScript 时,我们需要不时地进行重构。 重构意味着在保持兼容性的同时重写部分代码。 由于 JavaScript 已经经历了许多阶段和升级,重构也利用了所提供的新特性,使我们的应用运行得更快、更可靠。 重构的一个例子如下:

function appendPrefix(prefix, input) {    const result = [];    for (var i = 0; i < input.length; i++) {       result.push(prefix + input[i]);    }    return result; }

这段代码只是向输入数组中的所有元素添加一个前缀 让我们这样称呼它:

appendPrefix('Hi! ', ['Miku', 'Rin', 'Len']);

我们将得到以下输出:

![Figure 7.60: Output after running an array code ](img/C14587_07_60.jpg)

######  7.60:运行数组代码后的输出

在重构过程中我们可以用更少的代码编写前面的函数同时仍然保留所有的特性:

function appendPrefix(prefix, input) {    return input.map((inputItem) => {       return prefix + inputItem;    }); }

当我们再次调用它时会发生什么? 让我们来看看:

appendPrefix('Hi! ', ['Miku', 'Rin', 'Len']); ```

我们仍然会得到相同的输出:

Figure 7.61: Getting the same output after refactoring the code

图 7.61:重构代码后得到相同的输出

Activity 10: Refactoring Functions to Use Modern JavaScript FeaturesActivity 10: Refactoring Functions to Use Modern JavaScript Features

你最近加入了一家公司。 分配给您的第一个任务是重构许多遗留模块。 打开该文件,可以看到现有代码已经使用遗留的 JavaScript 方法编写。 您需要重构该文件中的所有函数,并确保它仍然能够通过所需的测试。

执行以下步骤来完成此活动:

  1. 使用 node.js 运行Activity10.js检查测试是否通过。
  2. 使用includes数组重构itemExist函数。
  3. 使用array push将一个新项目添加到pushunique功能的底部。
  4. createFilledArray中使用array.fill来填充数组的初始值。
  5. removeFirst函数中使用array.shift删除第一项。
  6. removeLast函数中使用array.pop删除最后一项。
  7. 使用cloneArray中的扩展运算符来复制我们的数组。
  8. 使用ES6类重构Food类。
  9. After refactoring, run the code to observe that the same output is generated as it was by the legacy code.

    请注意

    这个活动的解决方案可以在 611 页找到。

在这次活动中,我们学习了如何通过使用重构函数来使用现代 JavaScript 函数。 我们已经成功地学会了如何在保持代码兼容性的同时重写代码。

小结

在这一章中,我们首先看看如何在 JavaScript 中构造和操作数组和对象。 然后,我们研究了使用扩展运算符连接数组和对象的方法。 使用扩展运算符可以避免编写没有循环的函数。 之后,我们讨论了用 JavaScript 实现 OOP 的方法。 通过使用这些类和类继承,我们可以构建复杂的应用,而不必编写大量重复的代码。 我们还研究了 Array、Map、Set、Regex、Date 和 Math 的内置方法。 当我们需要处理大量不同类型的数据时,这些是非常有用的。 最后,符号、迭代器、生成器和代理为我们的程序动态和整洁提供了巨大的可能性。 关于高级 JavaScript 的章节到此结束。 在下一章中,我们将讨论 JavaScript 中的异步编程。