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

来自菜鸟教程
跳转至:导航、​搜索

介绍

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

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

在本教程中,您将通过查看 filtermapreduce 来更深入地了解 JavaScript 中的函数式编程。

先决条件

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

  • 对 JavaScript 的工作理解。 您可以查看 如何在 JavaScript 系列中编写代码以获取更多信息。
  • 了解如何在 JavaScript 中构建和实现 for 循环。 这篇关于 JavaScript 中的循环的 文章是一个很好的起点。
  • Node.js 安装在本地,您可以按照【X65X】如何安装Node.js 并创建本地开发环境【X134X】来完成。

第 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 步 — 了解凯撒密码和加密和解密

在下面的代码片段中,您将使用数组方法 mapreducefilter 来加密和解密字符串。

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

加密字符串的意思是:“加扰它以使其难以阅读而不加扰。” 这样,即使有人在听并且他们确实拦截了您的消息,在他们解读之前,它仍将保持不可读。

有不同的加密方法,Caesar cipher 是对这样的字符串进行加扰的一种方法。 此密码可在您的代码中使用。 创建一个名为 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 gist 包含由 Evan Hahn 创建的这个凯撒密码函数的原始代码。

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

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

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

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

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 gist 包含 Peleke Sengstacke 创建的 tryAllisEnglish 的原始代码。

确保将这个英语中最常见的1000个单词列表保存为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 个常量变量:一个名为 key 的值为 12 和一个名为 messages 的数组:

const key = 12;

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

现在创建一个名为 encryptedMessages 的常量。 在 messages 上使用 map 函数:

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 方法在 messages 上使用 caesar 函数对每个字符串进行加密,并自动将结果存储在新数组中。

上述代码运行后,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 被赋予一个回调,它也获取每个字符串。 不同之处在于,如果回调返回 truefilter 只会将项目保存在数组中。

您可以重构上面的代码片段以使用 filter 而不是 forEachlikelyPossibilities 变量将不再是空数组。 相反,将其设置为等于 possibilities 数组。 在 possibilities 上调用 filter 方法:

const likelyPossibilities = possibilities.filter()

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

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

})

在此函数中,使用 return 语句返回 isEnglish 的结果,并将 string 作为其参数传入:

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

如果 isEnglish(string) 返回 true,则 filterstring 保存在新的 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 变量。 在 prices 上调用 reduce 方法:

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

prices.reduce()

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

prices.reduce(function (totalPrice, nextPrice) {

})

为了进一步分解,totalPrice 类似于第一个示例中的 total。 这是迄今为止看到的所有收到的价格相加后的总价格。

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

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

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
})

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

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 的常量变量。 在 courses 上调用 reduce 方法。 回调函数应该有两个参数: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);

这会生成输出:

OutputThe 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 并在 names 上调用 reduce 方法:

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 的第一个回调参数将是一个数组。 将 titleCasedNamename 的大写版本)推送到此数组并返回 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 将小写名称数组转换为单个大写名称数组。 这仍然是有效的用例,因为大写名称的单个列表仍然是单个结果。 它恰好是一个集合,而不是原始类型。

结论

在本教程中,您学习了如何使用 mapfilterreduce 编写更具可读性的代码。 使用 for 循环没有任何问题。 但是,通过这些函数提高抽象级别,必然会为可读性和可维护性带来立竿见影的好处。

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