如何在 JavaScript 中使用 map()、filter() 和 reduce()

介绍

JavaScript 中的功能编程有利于代码可读性、可维护性和可测试性. 功能编程思维的工具之一是编程以数组处理风格。

在许多情况下,这很有用,例如(https://reactjs.org/docs/lists-and-keys.html),使用过滤器去除外部数据,并使用减少这些函数,称为Array Extras,是对for循环的抽象化。

在本教程中,您将通过查看过滤地图减少来深入了解JavaScript中的功能编程。

前提条件

要完成本教程,您将需要以下内容:

您可以查看 How to Code in JavaScript系列以获取更多信息

步骤 1 – 使用forEach迭代

for 循环用于在数组中的每个项目上重复,通常在途中对每个项目进行一些操作。

一个例子是将一个数组中的每个字符串资本化。

1const strings = ['arielle', 'are', 'you', 'there'];
2const capitalizedStrings = [];
3
4for (let i = 0; i < strings.length; i += 1) {
5    const string = strings[i];
6    capitalizedStrings.push(string.toUpperCase());
7}
8
9console.log(capitalizedStrings);

在此片段中,您将开始使用一组名为strings的下层句子,然后启动一个名为capitalizedStrings的空格数组,而capitalizedStrings数组将存储被资本化的字符串。

for循环中,每个迭代的下一个字符串被资本化并推到capitalizedStrings

forEach函数可以用来使这个代码更简洁,这是一个自动通过列表循环的数组方法,换句话说,它处理初始化和增量计数的细节。

相反,你可以手动索引到字符串,你可以拨打forEach,并在每个迭代中收到下一个字符串。

1const strings = ['arielle', 'are', 'you', 'there'];
2const capitalizedStrings = [];
3
4strings.forEach(function (string) {
5    capitalizedStrings.push(string.toUpperCase());
6})
7
8console.log(capitalizedStrings);

但是它消除了对i计数器的需要,使您的代码更易于阅读。

这也引入了一种主要的模式,你会看到一次又一次,即:最好在Array.prototype上使用抽象细节的方法,如初始化和增量计数。这样,你可以专注于重要的逻辑。有几种其他数组方法,本文将讨论。前进,你将使用加密和解密来充分证明这些方法能做什么。

步骤2 – 了解凯撒加密器和加密和解密

在下面的片段中,您将使用数组方法地图,减少过滤来加密和解密字符串。

如果你向朋友发送一个像这是我的超级秘密消息这样的正常消息,而其他人得到它,他们可以立即读取消息,尽管他们不是预期的收件人。

加密字符串的意思是:编写字符串以使其难以读取而不编写字符串。这样,即使有人正在听,并且他们拦截你的消息,它仍然不可读,直到他们编写字符串。

加密有不同的方法,而Caesar加密器是一种方法来编码这样的字符串。 此加密器可用于您的代码。 创建一个名为Caesar的恒定变量。 若要加密您的代码中的字符串,请使用下面的函数:

 1var caesarShift = function (str, amount) {
 2  if (amount < 0) {
 3    return caesarShift(str, amount + 26);
 4  }
 5
 6  var output = "";
 7
 8  for (var i = 0; i < str.length; i++) {
 9    var c = str[i];
10
11    if (c.match(/[a-z]/i)) {
12      var code = str.charCodeAt(i);
13
14      if (code >= 65 && code <= 90) {
15        c = String.fromCharCode(((code - 65 + amount) % 26) + 65);
16      }
17
18      else if (code >= 97 && code <= 122) {
19        c = String.fromCharCode(((code - 97 + amount) % 26) + 97);
20      }
21    }
22
23    output += c;
24  }
25
26  return output;
27};

本 GitHub 酵母包含由 Evan Hahn 创建的 Caesar 加密函数的原始代码.

要用凯撒加密器加密,你必须选择一个键n,在1和25之间,并用原始字符串中的每个字母替换一个n字母,进一步在字母中。

像这样的代替字母使原来的字符串无法读取. 由于字符串是通过移动字母来扭曲的,它们可以通过将它们移动回来而扭曲。 如果你收到一个消息,你知道是用 2 个密钥加密的,你需要做的就是将字符转移回两个空间。

1const encryptedString = caesarShift('this is my super-secret message.', 2);

在这个代码中,消息通过将每个字母移动到前面的2个字母:a变成c;s变成u;等等 要查看结果,请使用console.logencryptedString打印到控制台:

1const encryptedString = caesarShift('this is my super-secret message.', 2);
2
3console.log(encryptedString);

为了引用上面的例子,消息这是我的超级秘密信息成为vjku ku oa uwrgt-ugetgv oguucig的杂乱消息。

不幸的是,这种形式的加密很容易被打破。用凯撒加密器加密的任何字符串的解密方法之一就是用每一个可能的密钥来解密它。

对于一些代码示例来说,你需要解密一些加密的消息,可以使用这个tryAll函数来做到这一点:

 1const tryAll = function (encryptedString) {
 2    const decryptionAttempts = []
 3
 4    while (decryptionAttempts.length < 26) {
 5        const decryptionKey = -decryptionAttempts.length;
 6        const decryptionAttempt = caesarShift(encryptedString, decryptionKey);
 7
 8        decryptionAttempts.push(decryptionAttempt)
 9    }
10
11    return decryptionAttempts;
12};

上面的函数采用一个加密的字符串,并返回每个可能的解密的数组。其中一个结果将是你想要的字符串。

要扫描一系列26种可能的解密是很困难的,可以消除那些绝对不正确的,你可以使用这个函数isEnglish来做到这一点:

 1'use strict'
 2const fs = require('fs')
 3
 4const _getFrequencyList = () => {
 5    const frequencyList = fs.readFileSync(`${__dirname}/eng_10k.txt`).toString().split('\n').slice(1000)
 6    const dict = {};
 7
 8    frequencyList.forEach(word => {
 9        if (!word.match(/[aeuoi]/gi)) {
10            return;
11        }
12
13        dict[word] = word;
14    })
15
16    return dict;
17}
18
19const isEnglish = string => {
20    const threshold = 3;
21
22    if (string.split(/\s/).length < 6) {
23        return true;
24    } else {
25        let count = 0;
26        const frequencyList = _getFrequencyList();
27
28        string.split(/\s/).forEach(function (string) {
29            const adjusted = string.toLowerCase().replace(/\./g, '')
30
31            if (frequencyList[adjusted]) {
32                count += 1;
33            }
34        })
35
36        return count > threshold;
37    }
38}

[本GitHub gist包含由Peleke Sengstacke创建的tryAllisEnglish的原始代码。

請確保儲存 這個英文最常見的1000個單詞列表eng_10k.txt

您可以将所有这些函数包含在同一个JavaScript文件中,或者您可以将每个函数导入为模块。

isEnglish函数读取一个字符串,计算该字符串中最常见的1000个英语单词的数量,如果在句子中找到超过3个字,则将字符串归类为英文。

过滤器的部分中,您将使用isEnglish函数。

您将使用这些函数来展示数组方法地图过滤减少的工作方式。

步骤 3 — 使用地图来转换数组

重塑一个for循环使用forEach暗示了这种风格的优点,但仍有改进的余地。在上一个例子中,capitalizedStrings数组在调用中被更新到forEach。在这方面没有什么固有错误的。

在这种情况下,你想将每个字符串的字符串转换为其资本化版本,这是一个非常常见的循环的用例:把所有东西放在一个数组中,把它转换成其他东西,然后在一个新的数组中收集结果。

将一个数组中的每个元素转换成一个新的元素,并收集结果被称为地图。JavaScript为这个用例具有内置的功能,称为mapforEach方法被用来,因为它抽象了管理迭代变量i的需要,这意味着你可以专注于真正重要的逻辑。同样,map也被用来,因为它抽象了初始化一个空的数组,并推到它。

在最后的解释之前,让我们看看一个快速的演示。在下面的例子中,将使用加密功能。你可以使用for循环或forEach

要演示如何使用地图函数,创建 2 个恒定变量:一个称为密钥,其值为 12,一个称为消息的数组:

1const key = 12;
2
3const messages = [
4    'arielle, are you there?',
5    'the ghost has killed the shell',
6    'the liziorati attack at dawn'
7]

现在创建一个称为加密消息的常数,在消息中使用地图函数:

1const encryptedMessages = messages.map()

地图中,创建具有参数字符串的函数:

1const encryptedMessages = messages.map(function (string) {
2
3})

在此函数中,创建一个返回声明,该声明将返回加密函数caesarShift

1const encryptedMessages = messages.map(function (string) {
2    return caesarShift(string, key);
3})

加密消息打印到控制台,以查看结果:

1const encryptedMessages = messages.map(function (string) {
2    return caesarShift(string, key);
3})
4
5console.log(encryptedMessages);

消息中使用地图方法以凯撒函数加密每个字符串,并自动将结果存储在新数组中。

上面的代码运行后,‘加密消息’看起来像:‘[‘mduqxxq, mdq kag ftqdq?’,‘ftq staef tme Toshxqp ftq etqxx’,‘ftq xuluadmfu mffmow mfiz pmiz’。

您可以使用箭头函数重构加密消息,使您的代码更简洁:

1const encryptedMessages = messages.map(string => caesarShift(string, key));

现在,你已经对地图是如何工作的全面了解了,你可以使用过滤数组方法。

步骤 4 — 使用过滤器从数组中选择值

另一个常见的模式是使用for循环来处理数组中的项目,但只推/保留一些数组项目。

在原始的 JavaScript 中,这可能看起来像:

 1const encryptedMessage = 'mduqxxq, mdq kag ftqdq?';
 2
 3const possibilities = tryAll(encryptedMessage);
 4
 5const likelyPossibilities = [];
 6
 7possibilities.forEach(function (decryptionAttempt) {
 8    if (isEnglish(decryptionAttempt)) {
 9        likelyPossibilities.push(decryptionAttempt);
10    }
11})

tryAll函数用于解密加密消息,这意味着你会得到26种可能性。

由于大多数解密尝试都无法读取,因此使用一个forEach循环来检查每个解密的字符串是否具有isEnglish函数。

这是一个常见的使用案例,所以,它有一个内置的名为过滤器。 与地图一样,过滤器得到回调,这也收到了每个字符串。

您可以重塑上面的代码片段以使用过滤器而不是forEachprobablePossibilities变量将不再是一个空数组。

1const likelyPossibilities = possibilities.filter()

过滤器中,创建一个函数,该函数采用称为字符串的参数:

1const likelyPossibilities = possibilities.filter(function (string) {
2
3})

在此函数中,使用返回语句返回isEnglish的结果,以string作为其参数输入:

1const likelyPossibilities = possibilities.filter(function (string) {
2    return isEnglish(string);
3})

如果 'isEnglish(string)' 返回 'true',则 'filter' 会将'string' 保存到新的 'likelyPossibilities' 数组中。

由于这个回复称为isEnglish,这个代码可以进一步重构,以便更简洁:

1const likelyPossibilities = possibilities.filter(isEnglish);

减少方法是另一个非常重要的抽象。

步骤 5 — 使用减少将数组转换为单个值

在数组上迭代以将其元素收集成单个结果是一个非常常见的用例。

一个很好的例子是使用一个for循环来重复一个数组,并将所有数组合在一起:

1const prices = [12, 19, 7, 209];
2
3let totalPrice = 0;
4
5for (let i = 0; i < prices.length; i += 1) {
6    totalPrice += prices[i];
7}
8
9console.log(`Your total is ${totalPrice}.`);

「價格」中的數字被環繞,每個數字被添加到「總價格」。

您可以用「減少」重塑上述循環,您將不再需要「總價格」變量,請在「價格」上使用「減少」方法:

1const prices = [12, 19, 7, 209];
2
3prices.reduce()

减少方法将具有回调函数. 与地图过滤器不同,转换为减少的回调将接受两个参数:总累积价格和将添加到总数中的下一个价格。

1prices.reduce(function (totalPrice, nextPrice) {
2
3})

为了进一步打破这一点,‘totalPrice’在第一个示例中就像‘total’一样,它是迄今为止收到的所有价格加起来之后的总价格。

与上一个示例相比,‘nextPrice’ 对应于‘prices(i)’。 请记住,‘map’ 和‘reduce’ 会自动将该值索引到数组中,并自动将该值传递给其回调。

在函数中包含两个console.log语句,将totalPricenextPrice打印到控制台:

1prices.reduce(function (totalPrice, nextPrice) {
2    console.log(`Total price so far: ${totalPrice}`)
3    console.log(`Next price to add: ${nextPrice}`)
4})

您需要更新「總價格」以包含每個新的「下一個價格」:

1prices.reduce(function (totalPrice, nextPrice) {
2    console.log(`Total price so far: ${totalPrice}`)
3    console.log(`Next price to add: ${nextPrice}`)
4
5    totalPrice += nextPrice
6})

就像「地圖」和「減少」一樣,每一次重複,都需要返回一個值. 在這種情況下,該值是「總價格」。

1prices.reduce(function (totalPrice, nextPrice) {
2    console.log(`Total price so far: ${totalPrice}`)
3    console.log(`Next price to add: ${nextPrice}`)
4
5    totalPrice += nextPrice
6
7    return totalPrice
8})

减少方法需要两个参数. 第一个参数是已经创建的回调函数. 第二个参数是将作为总价格的起始值的数字。

1prices.reduce(function (totalPrice, nextPrice) {
2    console.log(`Total price so far: ${totalPrice}`)
3    console.log(`Next price to add: ${nextPrice}`)
4
5    totalPrice += nextPrice
6
7    return totalPrice
8}, 0)

正如你现在所看到的,减少可以用来收集数组成总数,但减少是多功能的,可以用来将数组变成任何单个结果,而不仅仅是数字值。

例如,减少可以用来构建一个字符串。 要在行动中看到这一点,先创建一个字符串的数组。 下面的示例使用一系列名为课程的计算机科学课程:

1const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];

创建一个名为课程的常态变量,在课程上调用减少方法,回调函数应该有两个参数:课程列表课程:

1const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
2
3const curriculum = courses.reduce(function (courseList, course) {
4
5});

courseList将需要更新,以包括每一个新的course:

1const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
2
3const curriculum = courses.reduce(function (courseList, course) {
4    return courseList += `\n\t${course}`;
5});

\n\t将创建一个新行和卡,在每个跑步之前进行入口。

為「減少」的第一個論點(回應函數)已完成. 因為正在構建一個字符串,而不是一個數字,第二個論點也將是一個字符串。

下面的示例使用计算机科学课程由:组成,作为减少的第二个参数。

1const courses = ['Introduction to Programming', 'Algorithms & Data Structures', 'Discrete Math'];
2
3const curriculum = courses.reduce(function (courseList, course) {
4    return courseList += `\n\t${course}`;
5}, 'The Computer Science curriculum consists of:');
6
7console.log(curriculum);

这就产生了产量:

1[secondary_label Output]
2The Computer Science curriculum consists of:
3    Introduction to Programming
4    Algorithms & Data Structures
5    Discrete Math

如前所述,减少是多功能的,可以用来将一个数组转化为任何一种单个结果,这个单个结果甚至可以成为一个数组。

创建一个系列的字符串:

1const names = ['arielle', 'jung', 'scheherazade'];

titleCase函数将以字符串中的第一个字母为首:

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}

titleCase通过抓住一个字符串的第一个字母在0索引中,使用该字母的toUpperCase,抓住其余的字符串,并将一切合并在一起。

有了titleCase,创建一个名为titleCased的恒定变量,将其设置为名称,然后在名称上调用减少方法:

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}
11
12const titleCased = names.reduce()

减少方法将有一个回调函数,将titleCasedNamesname作为参数:

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}
11
12const titleCased = names.reduce(function (titleCasedNames, name) {
13
14})

在回调函数中,创建一个名为titleCasedName的恒定变量,调用titleCase函数,然后输入name作为参数:

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}
11
12const titleCased = names.reduce(function (titleCasedNames, name) {
13    const titleCasedName = titleCase(name);
14})

这将使每个名称在名称中发挥作用。titleCasedNames回调函数的第一个回调参数将是一个数组。 将titleCasedName(名称的资本化版本)推到这个数组,然后返回titleCaseNames:

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}
11
12const titleCased = names.reduce(function (titleCasedNames, name) {
13    const titleCasedName = titleCase(name);
14
15    titleCasedNames.push(titleCasedName);
16
17    return titleCasedNames;
18})

减少方法需要两个参数. 首先是回调函数完成了. 由于此方法正在创建一个新数组,初始值将是一个空数组。

 1const names = ['arielle', 'jung', 'scheherazade'];
 2
 3const titleCase = function (name) {
 4    const first = name[0];
 5    const capitalizedFirst = first.toUpperCase();
 6    const rest = name.slice(1);
 7    const letters = [capitalizedFirst].concat(rest);
 8
 9    return letters.join('');
10}
11
12const titleCased = names.reduce(function (titleCasedNames, name) {
13    const titleCasedName = titleCase(name);
14
15    titleCasedNames.push(titleCasedName);
16
17    return titleCasedNames;
18}, [])
19
20console.log(titleCased);

运行您的代码后,它将产生下列资本化名称:

1[secondary_label Output]
2["Arielle", "Jung", "Scheherazade"]

您使用减少将一组较低的姓名转换为一组标题的姓名。

以前的例子证明,减少可以用来将数字列表变成一个单一的总和,也可以用来将字符串列表变成一个单一的字符串。在这里,你使用了减少来将一组较低的名称变成一组较高的名称。

结论

在本教程中,你已经学会了如何使用地图过滤减少来编写更可读的代码. 使用循环没有什么不对劲,但通过这些功能提高抽象水平,可以立即提高可读性和可维护性。

从这里,你可以开始探索其他数组方法,如flattenflatMap。这篇名为Flatten Arrays in Vanilla JavaScript with flat() and flatMap()(https://andsky.com/tech/tutorials/js-flat-flatmap)的文章是一个很好的起点。

Published At
Categories with 技术
comments powered by Disqus