介绍
JavaScript 中的函数式编程有利于代码的可读性、可维护性和可测试性。函数式编程思维中的一种工具是以数组处理风格进行编程。这需要将数组作为您的基本数据结构。然后,你的程序就变成了对数组元素的一系列操作。
这在很多情况下都很有用,例如使用将 AJAX 结果映射到 React 组件map
,使用 删除无关数据filter
,以及使用reduce
。这些称为“Array Extras”的函数是对for
循环的抽象。使用这些功能,您for
无所不能,反之亦然。
在本教程中,您将通过在考虑看看开发的JavaScript函数式编程更深入的了解filter
,map
和reduce
。
先决条件
要完成本教程,您将需要以下内容:
- 对 JavaScript 的有效理解。您可以查看如何在 JavaScript 中编码系列以获取更多信息。
- 了解如何
for
在 JavaScript 中构建和实现循环。该文章for
在JavaScript中环路是一个伟大的地方开始。 - Node.js 安装在本地,您可以按照如何安装 Node.js 并创建本地开发环境来完成。
步骤 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 — 了解凯撒密码和加密解密
在下面的代码片段,您可以使用数组方法map
,reduce
以及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
变成d
;c
变成e
;等等。
像这样替换字母会使原始字符串不可读。由于字符串是通过移位字母来加扰的,因此可以通过将它们移回来解密它们。如果您收到一条消息,您知道它是用密钥 2 加密的,那么解密所需要做的就是将字母后移两个空格。所以,c
变成了a
;d
变成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
函数。
您将使用这些功能来演示如何阵列的方法map
,filter
和reduce
工作。该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
. 使用该map
功能messages
:
const encryptedMessages = messages.map()
在 中map
,创建一个具有参数的函数string
:
const encryptedMessages = messages.map(function (string) {
})
在这个函数内部,创建一个return
返回密码函数的语句caesarShift
。该caesarShift
函数应该有string
和key
作为它的参数:
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
给出一个回调,它也获取每个字符串。不同之处在于,filter
如果回调返回,则只会将项目保存在数组中true
。
您可以重构上面的代码片断使用filter
代替forEach
。该likelyPossibilities
变量将不再是一个空数组。相反,将它设置为等于possibilities
数组。调用filter
方法possibilities
:
const likelyPossibilities = possibilities.filter()
在 中filter
,创建一个函数,该函数接受一个名为 的参数string
:
const likelyPossibilities = possibilities.filter(function (string) {
})
在此函数中,使用一条return
语句返回isEnglish
withstring
作为参数传入的结果:
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
循环通过并将每个数字添加到totalPrice
。该reduce
方法是此用例的抽象。
您可以使用reduce
. 您将不再需要该totalPrice
变量。调用reduce
方法prices
:
const prices = [12, 19, 7, 209];
prices.reduce()
该reduce
方法将持有一个回调函数。与map
and不同filter
,传递给的回调reduce
接受两个参数:总累计价格和数组中要添加到总价中的下一个价格。这将是totalPrice
和nextPrice
分别为:
prices.reduce(function (totalPrice, nextPrice) {
})
进一步细分,totalPrice
就像total
在第一个示例中一样。这是将目前看到的所有价格相加后的总价。
与前面的示例相比,nextPrice
对应于prices[i]
. 回想一下map
并reduce
自动索引到数组中,并自动将此值传递给它们的回调。该reduce
方法执行相同的操作,但将该值作为第二个参数传递给其回调。
console.log
在打印totalPrice
和nextPrice
到控制台的函数中包含两个语句:
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
})
就像使用map
and 一样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
。回调函数应该有两个参数:courseList
和course
:
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
并调用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
方法将有一个回调函数,它接受titleCasedNames
和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) {
})
在回调函数中,创建一个名为 的常量变量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
将一个小写名称数组转换为一个大写名称数组。这仍然是有效的用例,因为大写名称的单个列表仍然是单个结果。它恰好是一个集合,而不是一个原始类型。
结论
在本教程中,你已经学会了如何使用map
,filter
以及reduce
编写更可读的代码。使用for
循环没有任何问题。但是,必然地,提高这些函数的抽象级别会给可读性和可维护性带来直接的好处。
从这里,您可以开始探索其他数组方法,例如flatten
和flatMap
。这篇名为Flatten Arrays in Vanilla JavaScript with flat() 和 flatMap() 的文章是一个很好的起点。