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

介绍

JavaScript 中的函数式编程有利于代码的可读性、可维护性和可测试性。函数式编程思维中的一种工具是以数组处理风格进行编程。这需要将数组作为您的基本数据结构。然后,你的程序就变成了对数组元素的一系列操作。

这在很多情况下都很有用,例如使用将 AJAX 结果映射到 React 组件map,使用 删除无关数据filter,以及使用reduce这些称为“Array Extras”的函数是对for循环的抽象使用这些功能,您for无所不能,反之亦然。

在本教程中,您将通过在考虑看看开发的JavaScript函数式编程更深入的了解filtermapreduce

先决条件

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

步骤 1 — 迭代 forEach

for循环用于遍历数组中的每一项。通常,沿途对每个项目都做一些事情。

一个例子是将数组中的每个字符串大写。

const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];

for (let i = 0; i < strings.length; i += 1) {
    const string = strings[i];
    capitalizedStrings.push(string.toUpperCase());
}

console.log(capitalizedStrings);

在此代码段中,您从一组名为 的小写短语开始strings然后,capitalizedStrings初始化一个名为的空数组capitalizedStrings数组将存储大写的字符串。

for循环内部,每次迭代的下一个字符串都大写并推送到capitalizedStrings在循环结束时,capitalizedStrings包含 中每个单词的大写版本strings

forEach函数可用于使此代码更加简洁。这是一种“自动”循环遍历列表的数组方法。换句话说,它处理初始化和递增计数器的细节。

strings您可以forEach在每次迭代时调用并接收下一个字符串,而不是上面手动索引到 的地方更新后的版本看起来像这样:

const strings = ['arielle', 'are', 'you', 'there'];
const capitalizedStrings = [];

strings.forEach(function (string) {
    capitalizedStrings.push(string.toUpperCase());
})

console.log(capitalizedStrings);

这非常接近初始功能。但它消除了对i计数器的需要,使您的代码更具可读性。

这也引入了一个你会一次又一次看到的主要模式。即:最好在Array.prototype抽象的细节上使用方法,例如初始化和递增计数器。这样,您就可以专注于重要的逻辑。本文将讨论其他几种数组方法。展望未来,您将使用加密和解密来充分展示这些方法的功能。

步骤 2 — 了解凯撒密码和加密解密

在下面的代码片段,您可以使用数组方法mapreduce以及filter加密和解密字符串。

首先了解什么是加密很重要。如果您向'this is my super-secret message'朋友发送一条普通消息,而其他人得到了它,即使他们不是预期的收件人,他们也可以立即阅读该消息。如果您发送敏感信息(例如密码),有人可能会监听这些信息,则这是一件坏事。

加密一个字符串意味着:“在不加扰的情况下对其进行加扰使其难以阅读。” 这样,即使有人在听并且他们确实拦截了您的消息,在他们解读之前,它仍然无法阅读。

有不同的加密方法,凯撒密码是一种对这样的字符串进行加扰的方法。此密码可用于您的代码。创建一个名为 的常量变量caesar要加密代码中的字符串,请使用以下函数:

var caesarShift = function (str, amount) {
  if (amount < 0) {
    return caesarShift(str, amount + 26);
  }

  var output = "";

  for (var i = 0; i < str.length; i++) {
    var c = str[i];

    if (c.match(/[a-z]/i)) {
      var code = str.charCodeAt(i);

      if (code >= 65 && code <= 90) {
        c = String.fromCharCode(((code - 65 + amount) % 26) + 65);
      }

      else if (code >= 97 && code <= 122) {
        c = String.fromCharCode(((code - 97 + amount) % 26) + 97);
      }
    }

    output += c;
  }

  return output;
};

这个 GitHub 要点包含 Evan Hahn 创建的 Caesar 密码函数的原始代码。

要使用凯撒密码进行加密,您必须选择一个n介于 1 到 25 之间的密钥,并将原始字符串中的每个字母替换n为字母表中更靠后的一个字母。因此,如果您选择键 2,则a变为c; b变成dc变成e等等。

像这样替换字母会使原始字符串不可读。由于字符串是通过移位字母来加扰的,因此可以通过将它们移回来解密它们。如果您收到一条消息,您知道它是用密钥 2 加密的,那么解密所需要做的就是将字母后移两个空格。所以,c变成了ad变成b等等。要查看 Caesar 密码的实际效果,请调用该caesarShift函数并将字符串'this is my super-secret message.'作为第一个参数传入,将数字2作为第二个参数传入

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

在此代码中,通过将每个字母向前移动 2 个字母来对消息进行加扰:a变成c; s变成 u等要查看结果,请使用console.log打印encryptedString到控制台:

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

console.log(encryptedString);

引用上面的例子,消息'this is my super-secret message'变成了加扰消息'vjku ku oa uwrgt-ugetgv oguucig.'

不幸的是,这种形式的加密很容易被破解。解密使用凯撒密码加密的任何字符串的一种方法是尝试使用每个可能的密钥对其进行解密。结果之一将是正确的。

对于接下来的一些代码示例,您需要解密一些加密的消息。tryAll功能可用于执行此操作:

const tryAll = function (encryptedString) {
    const decryptionAttempts = []

    while (decryptionAttempts.length < 26) {
        const decryptionKey = -decryptionAttempts.length;
        const decryptionAttempt = caesarShift(encryptedString, decryptionKey);

        decryptionAttempts.push(decryptionAttempt)
    }

    return decryptionAttempts;
};

上面的函数接受一个加密的字符串,并返回一个包含每个可能解密的数组。这些结果之一将是您想要的字符串。所以,这总是会破解密码。

扫描包含 26 个可能解密的数组是一项挑战。有可能消除那些绝对不正确的。您可以使用此功能isEnglish来执行此操作:

'use strict'
const fs = require('fs')

const _getFrequencyList = () => {
    const frequencyList = fs.readFileSync(`${__dirname}/eng_10k.txt`).toString().split('\n').slice(1000)
    const dict = {};

    frequencyList.forEach(word => {
        if (!word.match(/[aeuoi]/gi)) {
            return;
        }

        dict[word] = word;
    })

    return dict;
}

const isEnglish = string => {
    const threshold = 3;

    if (string.split(/\s/).length < 6) {
        return true;
    } else {
        let count = 0;
        const frequencyList = _getFrequencyList();

        string.split(/\s/).forEach(function (string) {
            const adjusted = string.toLowerCase().replace(/\./g, '')

            if (frequencyList[adjusted]) {
                count += 1;
            }
        })

        return count > threshold;
    }
}

这GitHub的要点包含了对原始代码tryAll,并isEnglish通过Peleke Sengstacke创建。

确保将此最常见的 1,000 个英语单词列表保存eng_10k.txt.

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

isEnglish函数读取一个字符串,计算该字符串中出现的最常见的 1,000 个英语单词中有多少个,如果在句子中找到 3 个以上的单词,则将该字符串分类为英语。如果字符串包含来自该数组的少于 3 个单词,则将其丢弃。

在 部分中filter,您将使用该isEnglish函数。

您将使用这些功能来演示如何阵列的方法mapfilterreduce工作。map方法将在下一步中介绍。

第 3 步 –map用于转换数组

重构for循环以使用forEach这种风格的优点的提示。但仍有改进的余地。在前面的示例中,capitalizedStrings数组正在回调中更新为forEach这没有什么本质上的错误。但最好尽可能避免此类副作用。如果不必更新位于不同作用域中的数据结构,最好避免这样做。

在这种特殊情况下,您希望将每个字符串都strings转换为其大写版本。这是for循环的一个非常常见的用例:获取数组中的所有内容,将其转换为其他内容,然后将结果收集到一个新数组中。

将数组中的每个元素转换为一个新元素并收集结果称为映射。JavaScript 有一个用于此用例的内置函数,称为map. 使用该forEach方法是因为它抽象了管理迭代变量 的需要i这意味着您可以专注于真正重要的逻辑。类似地,map使用是因为它抽象了初始化一个空数组并推送到它。就像forEach接受一个对每个字符串值做某事的回调一样,接受一个对每个字符串值map做某事的回调。

在最终解释之前,让我们先看一个快速演示。在以下示例中,将使用加密函数。您可以使用for循环或forEach. 但最好map在这种情况下使用

为了演示如何使用该map函数,创建 2 个常量变量:一个名为key12 的数组和一个名为 的数组messages

const key = 12;

const messages = [
    'arielle, are you there?',
    'the ghost has killed the shell',
    'the liziorati attack at dawn'
]

现在创建一个名为encryptedMessages. 使用该map功能messages

const encryptedMessages = messages.map()

在 中map,创建一个具有参数的函数string

const encryptedMessages = messages.map(function (string) {

})

在这个函数内部,创建一个return返回密码函数语句caesarShiftcaesarShift函数应该有stringkey作为它的参数:

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

打印encryptedMessages到控制台查看结果:

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

console.log(encryptedMessages);

注意这里发生了什么。map方法用于使用messagescaesar函数对每个字符串进行加密,并自动将结果存储在一个新数组中。

上面的代码运行后,encryptedMessages看起来像:['mduqxxq, mdq kag ftqdq?', 'ftq staef tme wuxxqp ftq etqxx', 'ftq xuluadmfu mffmow mf pmiz']. 这是比手动推送到数组更高的抽象级别。

您可以encryptedMessages使用箭头函数进行重构,使您的代码更加简洁:

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

现在您已经彻底了解map工作原理,您可以使用filter数组方法。

第 4 步 – 使用filter从数组中选择值

另一种常见的模式是使用for循环来处理数组中的项目,但只推送/保留一些数组项目。通常,if语句用于决定保留哪些项目以及丢弃哪些项目。

在原始 JavaScript 中,这可能如下所示:

const encryptedMessage = 'mduqxxq, mdq kag ftqdq?';

const possibilities = tryAll(encryptedMessage);

const likelyPossibilities = [];

possibilities.forEach(function (decryptionAttempt) {
    if (isEnglish(decryptionAttempt)) {
        likelyPossibilities.push(decryptionAttempt);
    }
})

tryAll函数用于解密encryptedMessage. 这意味着你最终会得到 26 种可能性。

由于大多数解密尝试都无法读取,因此使用forEach循环来检查每个解密的字符串是否是英文的isEnglish函数。英文字符串被推送到一个名为likelyPossibilities.

这是一个常见的用例。所以,有一个内置的叫做filter. map,filter给出一个回调,它也获取每个字符串。不同之处在于,filter如果回调返回则只会将项目保存在数组中true

您可以重构上面的代码片断使用filter代替forEachlikelyPossibilities变量将不再是一个空数组。相反,将它设置为等于possibilities数组。调用filter方法possibilities

const likelyPossibilities = possibilities.filter()

在 中filter,创建一个函数,该函数接受一个名为 的参数string

const likelyPossibilities = possibilities.filter(function (string) {

})

在此函数中,使用一条return语句返回isEnglishwithstring作为参数传入的结果

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

如果isEnglish(string)返回true,则filter保存string在新likelyPossibilities数组中。

由于此回调调用isEnglish,因此可以进一步重构此代码以使其更加简洁:

const likelyPossibilities = possibilities.filter(isEnglish);

reduce方法是另一个抽象那就是知道很重要。

第 5 步 – 使用reduce将数组转换为单个值

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

一个很好的例子是使用for循环遍历数字数组并将所有数字加在一起:

const prices = [12, 19, 7, 209];

let totalPrice = 0;

for (let i = 0; i < prices.length; i += 1) {
    totalPrice += prices[i];
}

console.log(`Your total is ${totalPrice}.`);

中的数字prices循环通过并将每个数字添加到totalPricereduce方法是此用例的抽象。

您可以使用reduce. 您将不再需要该totalPrice变量。调用reduce方法prices

const prices = [12, 19, 7, 209];

prices.reduce()

reduce方法将持有一个回调函数。mapand不同filter,传递给的回调reduce接受两个参数:总累计价格和数组中要添加到总价中的下一个价格。这将是totalPricenextPrice分别为:

prices.reduce(function (totalPrice, nextPrice) {

})

进一步细分,totalPrice就像total在第一个示例中一样。这是将目前看到的所有价格相加后的总价。

与前面的示例相比,nextPrice对应于prices[i]. 回想一下mapreduce自动索引到数组中,并自动将此值传递给它们的回调。reduce方法执行相同的操作,但将该值作为第二个参数传递给其回调。

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

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

您需要更新totalPrice以包含每个新的nextPrice

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

    totalPrice += nextPrice
})

就像使用mapand 一样reduce,在每次迭代中,都需要返回一个值。在这种情况下,该值为totalPrice所以创建一个return语句totalPrice

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

    totalPrice += nextPrice

    return totalPrice
})

reduce方法有两个参数。第一个是已经创建的回调函数。第二个参数是一个数字,它将作为 的起始值totalPrice这对应const total = 0于前面的示例。

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

    totalPrice += nextPrice

    return totalPrice
}, 0)

正如您现在看到的,reduce可用于将一组数字收集为一个总和。reduce用途广泛。它可用于将数组转换为任何单个结果,而不仅仅是数值。

例如,reduce可用于构建字符串。要查看此操作,首先创建一个字符串数组。下面的示例使用了一组名为 的计算机科学课程courses

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

创建一个名为 的常量变量curriculum调用reduce方法courses回调函数应该有两个参数:courseListcourse

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

const curriculum = courses.reduce(function (courseList, course) {

});

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

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

const curriculum = courses.reduce(function (courseList, course) {
    return courseList += `\n\t${course}`;
});

\n\t将各之前创建缩进换行和标签course

reduce(回调函数)的第一个参数已完成。因为正在构造一个字符串,而不是一个数字,所以第二个参数也将是一个字符串。

下面的示例'The Computer Science curriculum consists of:'用作 的第二个参数reduce添加一条console.log语句打印curriculum到控制台:

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

const curriculum = courses.reduce(function (courseList, course) {
    return courseList += `\n\t${course}`;
}, 'The Computer Science curriculum consists of:');

console.log(curriculum);

这将生成输出:

Output
The Computer Science curriculum consists of: Introduction to Programming Algorithms & Data Structures Discrete Math

如前所述,reduce是多才多艺的。它可用于将数组转换为任何类型的单个结果。单个结果甚至可以是一个数组。

创建一个字符串数组:

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

titleCase函数将大写字符串中的第一个字母:

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

titleCase通过在0索引处抓取字符串的第一个字母toUpperCase在该字母上使用,抓取字符串的其余部分,并将所有内容连接在一起,将字符串中的第一个字母大写

随着titleCase到位,创建一个名为中的常量变量titleCased将其设置为等于names并调用reduce方法names

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce()

reduce方法将有一个回调函数,它接受titleCasedNamesname作为参数:

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {

})

在回调函数中,创建一个名为 的常量变量titleCasedName调用titleCase函数并name作为参数传入

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);
})

这将大写中的每个名称names回调函数的第一个回调参数titleCasedNames将是一个数组。推送titleCasedName( 的大写版本name)到这个数组并返回titleCaseNames

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);

    titleCasedNames.push(titleCasedName);

    return titleCasedNames;
})

reduce方法需要两个参数。首先是回调函数完成。由于此方法正在创建一个新数组,因此初始值将是一个空数组。此外,包括console.log将最终结果打印到屏幕上:

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

const titleCase = function (name) {
    const first = name[0];
    const capitalizedFirst = first.toUpperCase();
    const rest = name.slice(1);
    const letters = [capitalizedFirst].concat(rest);

    return letters.join('');
}

const titleCased = names.reduce(function (titleCasedNames, name) {
    const titleCasedName = titleCase(name);

    titleCasedNames.push(titleCasedName);

    return titleCasedNames;
}, [])

console.log(titleCased);

运行您的代码后,它将生成以下大写名称数组:

Output
["Arielle", "Jung", "Scheherazade"]

您曾经reduce将一组小写名称转换为一组标题大小写名称。

前面的例子证明reduce可以用于将数字列表转换为单个和,也可以用于将字符串列表转换为单个字符串。在这里,您曾经reduce将一个小写名称数组转换为一个大写名称数组。这仍然是有效的用例,因为大写名称的单个列表仍然是单个结果。它恰好是一个集合,而不是一个原始类型。

结论

在本教程中,你已经学会了如何使用mapfilter以及reduce编写更可读的代码。使用for循环没有任何问题但是,必然地,提高这些函数的抽象级别会给可读性和可维护性带来直接的好处。

从这里,您可以开始探索其他数组方法,例如flattenflatMap这篇名为Flatten Arrays in Vanilla JavaScript with flat() 和 flatMap() 的文章是一个很好的起点。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁