这些天来,函数式编程在开发界引起了轰动。并且有充分的理由:函数式技术可以帮助您编写更易于一目了然、重构和测试的声明性代码。
函数式编程的基石之一是它对列表和列表操作的特殊使用。这些事情正是它们听起来的样子:一系列事物以及您对它们所做的事情。但是功能性思维方式对待它们的方式与您预期的有所不同。
本文将仔细研究我喜欢称之为“三巨头”的列表操作:map、filter和reduce。了解这三个函数是编写干净的函数式代码的重要一步,它为函数式和反应式编程的强大技术打开了大门。
好奇的?让我们潜入水中。
- 从列表到列表的映射
- 过滤掉噪音
- 减少方法
- 放在一起:映射、过滤、减少和可链接性
- 结论和后续步骤
从列表到列表的映射
通常,我们发现自己需要获取一个数组并以完全相同的方式修改其中的每个元素。这方面的典型示例是对数字数组中的每个元素进行平方,从用户列表中检索名称,或对字符串数组运行正则表达式。
map 是一种为此而构建的方法。它在 上定义Array.prototype,因此您可以在任何数组上调用它,并且它接受回调作为其第一个参数。
for 的语法map如下所示。
1
2
3
|
let newArray = arr.map(callback(currentValue[, index[, array]]) {
// return element for newArray, after executing something
}[, thisArg]);
|
当您调用 map 一个数组时,它会在其中的每个元素上执行该回调,并返回一个包含回调返回的所有值的新数组。
在后台, map将三个参数传递给您的回调:
- 数组中的当前项
- 当前项的数组索引
- 你调用的整个 数组map
让我们看一些代码。
map在实践中
假设我们有一个应用程序可以维护您当天的一系列任务。每个 task 都是一个对象,每个都有一个 name and duration 属性:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// Durations are in minutes
const tasks = [
{
‘name’ : ‘Write for Envato Tuts+’,
‘duration’ : 120
},
{
‘name’ : ‘Work out’,
‘duration’ : 60
},
{
‘name’ : ‘Procrastinate on Duolingo’,
‘duration’ : 240
}
];
|
假设我们想创建一个只包含每个任务名称的新数组,这样我们就可以看看我们今天所做的一切。使用for循环,我们会这样写:
1
2
3
4
5
6
7
|
const task_names = [];
for (let i = 0, max = tasks.length; i < max; i += 1) {
task_names.push(tasks[i].name);
}
console.log(task_names) // [ ‘Write for Envato Tuts+’, ‘Work out’, ‘Procrastinate on Duolingo’ ]
|
JavaScript 还提供了一个 forEach循环。它的功能就像一个 for循环,但为我们管理检查循环索引和数组长度的所有麻烦:
1
2
3
4
5
6
7
|
const task_names = [];
tasks.forEach(function (task) {
task_names.push(task.name);
});
console.log(task_names) // [ ‘Write for Envato Tuts+’, ‘Work out’, ‘Procrastinate on Duolingo’ ]
|
使用map,我们可以简单地写:
1
2
3
4
5
|
const task_names = tasks.map(function (task, index, array) {
return task.name;
});
console.log(task_names) // [ ‘Write for Envato Tuts+’, ‘Work out’, ‘Procrastinate on Duolingo’ ]
|
在这里,我包含了index和 array参数来提醒您,如果您需要它们,它们就在那里。但是,由于我在这里没有使用它们,因此您可以将它们排除在外,并且代码可以正常运行。
在现代 JavaScript 中,一种更简洁的编写方式map是使用箭头函数。
1
2
3
|
const task_names = tasks.map(task => task.name)
console.log(task_names) // [‘Write for Envato Tuts+’, ‘Work out’, ‘Procrastinate on DuoLingo’]
|
return箭头函数是只有一条语句的单行函数的缩写形式。它没有比这更具可读性的了。
不同的方法之间有一些重要的区别:
- 使用map,您不必自己管理for循环的状态。
- 使用map,您可以直接对元素进行操作,而不必索引到数组中。
- 您不必创建一个新数组并将push其放入其中。map 一次性返回成品,因此我们可以简单地将返回值分配给一个新变量。
- 你必须记住在你的回调中包含一个 声明return 。如果你不这样做,你会得到一个新的数组,里面填充了undefined.
事实证明,我们今天要讨论的所有函数都具有这些特征。
我们不必手动管理循环状态的事实使我们的代码更简单,更易于维护。我们可以直接对元素进行操作,而不必索引数组,这一事实使事情更具可读性。
使用 forEach循环为我们解决了这两个问题。但 map仍然至少有两个明显的优势:
- forEach return undefined,因此它不会与其他数组方法链接。 map返回一个数组,因此您 可以将其与其他数组方法链接。
- map 返回 一个包含成品的数组,而不是要求我们在循环内改变一个数组。
将修改状态的位置保持在绝对最小值是函数式编程的一个重要原则。它使代码更安全、更易懂。
陷阱
您传递给的回调 map必须有一个明确的 return声明,否则 map会吐出一个充满 undefined. 记住包含一个 return值并不难,但也不难忘记。
如果你真的忘记了, map不要抱怨。相反,它会悄悄地交还一个空无一物的数组。像这样的静默错误很难调试。
幸运的是,这是 唯一的问题 map。但这是一个很常见的陷阱,我不得不强调:始终确保你的回调包含一个 return语句!
执行
阅读实现是理解的重要部分。因此,让我们编写自己的轻量级map以更好地了解幕后发生的事情。如果您想查看生产质量的实现,请查看MDN 上的 Mozilla 的 polyfill。
1
2
3
4
5
6
7
8
9
|
let map = function (array, callback) {
const new_array = [];
array.forEach(function (element, index, array) {
new_array.push(callback(element));
});
return new_array;
};
|
此代码接受一个数组和一个回调函数作为参数。然后它创建一个新数组,对我们传入的数组中的每个元素执行回调,将结果推送到新数组中,并返回新数组。如果您在控制台中运行它,您将获得与以前相同的结果。
当我们在底层使用 for 循环时,将其包装到一个函数中会隐藏细节并让我们使用抽象来代替。
这使我们的代码更具声明性——它说明 了 要做什么,而不是 如何去做。您会意识到这可以使您的代码更具可读性、可维护性和可调试性。
过滤掉噪音
我们的下一个数组操作是filter. 它确实像听起来那样:它需要一个数组并过滤掉不需要的元素。
过滤器的语法是:
1
2
3
|
let newArray = arr.filter(callback(currentValue[, index[, array]]) {
// return element for newArray, if true
}[, thisArg]);
|
就像map,filter传递你的回调三个参数:
- 当前项目
- 当前索引
- 你调用的 数组filter
考虑以下示例,该示例过滤掉任何少于 8 个字符的字符串。
1
2
3
|
const words = [‘Python’, ‘Javascript’, ‘Go’, ‘Java’, ‘PHP’, ‘Ruby’];
const result = words.filter(word => word.length < 8);
console.log(result);
|
预期结果将是:
1
|
[ ‘Python’, ‘Go’, ‘Java’, ‘PHP’, ‘Ruby’ ]
|
让我们重温一下我们的任务示例。假设我想获得一个仅包含我花了两个小时或更长时间才能完成的任务的列表,而不是提取每个任务的名称。
使用forEach,我们会写:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
const difficult_tasks = [];
tasks.forEach(function (task) {
if (task.duration >= 120) {
difficult_tasks.push(task);
}
});
console.log(difficult_tasks)
// [{ name: ‘Write for Envato Tuts+’, duration: 120 },
// { name: ‘Procrastinate on Duolingo’, duration: 240 }
// ]
|
有了filter,我们可以简单地写成:
1
|
const difficult_tasks = tasks.filter((task) => task.duration >= 120 );
|
就像map,filter让我们:
- forEach避免在orfor 循环内改变数组
- 将其结果直接分配给一个新变量,而不是推入我们在别处定义的数组
陷阱
map如果您希望它正常运行,您传递给的回调 必须包含一个 return 语句。使用 filter,您还必须包含一个 return 语句(除非您使用箭头函数),并且您必须确保它返回一个布尔值。
如果您忘记了 return 语句,您的回调将返回undefined,这 filter将无助地强制转换为false. 它不会抛出错误,而是会默默地返回一个空数组!
如果你走另一条路并返回一些不是明确的 true or false,那么 将尝试通过应用JavaScript 的类型强制规则filter来弄清楚你的意思。通常,这是一个错误。而且,就像忘记您的退货声明一样,这将是一个沉默的声明。
始终确保您的回调包含明确的 return 语句。并始终确保您的回调作为 filter 回报true或false. 你的理智会感谢你。
执行
再一次,理解一段代码的最好方法是……嗯,写它。让我们推出自己的轻量级filter. Mozilla 的好人也有一个工业级的 polyfill供你阅读。
01
02
03
04
05
06
07
08
09
10
11
12
13
|
const filter = function (array, callback) {
const filtered_array = [];
array.forEach(function (element, index, array) {
if (callback(element, index, array)) {
filtered_array.push(element);
}
});
return filtered_array;
};
|
减少方法
reduceJavaScript 中数组方法的语法是:
1
2
3
|
let newArray = arr.filter(callback(currentValue, accumulatedValue) {
// return the accumulated value, given the current and previous accumulated value
}, initialValue[, thisArg]);
|
map通过单独转换数组中的每个元素来创建一个新数组。filter通过删除不属于的元素创建一个新数组。 reduce另一方面,它获取数组中的所有元素并将它们简化为单个值。
就像mapand 一样filter, reduce 被定义在 Array.prototype任何数组上,所以你传递一个回调作为它的第一个参数。但它也需要第二个参数:开始组合所有数组元素的值。
reduce传递你的回调四个参数:
- 当前值
- 前一个值
- 当前索引
- 你调用 的数组reduce
请注意,回调 在每次迭代时都会获得一个先前的值。在第一次迭代中, 没有先前的值。这就是您可以选择传递 reduce初始值的原因:它充当第一次迭代的“先前值”,否则不会有一个。
最后,请记住 reduce返回单个值, 而不是包含单个项目的数组。这比看起来更重要,我将在示例中回到它。
reduce在实践中
假设您要查找数字列表的总和。使用循环,它看起来像这样:
1
2
3
4
5
6
7
8
|
let numbers = [1, 2, 3, 4, 5],
total = 0;
numbers.forEach(function (number) {
total += number;
});
console.log(total); // 15
|
虽然这 对forEach. reduce,reduce我们会写:
1
2
3
4
|
const total = [1, 2, 3, 4, 5].reduce(function (previous, current) {
return previous + current;
}, 0);
console.log(total); // 15
|
首先,我们调用 reduce 我们的数字列表。我们向它传递一个回调,它接受以前的值和当前值作为参数,并返回将它们相加的结果。因为我们 0作为第二个参数传递给 reduce,所以它会 previous在第一次迭代中使用它作为 的值。
使用箭头函数,我们可以这样写:
1
2
|
const total = [1, 2, 3, 4, 5].reduce((previous, current) => previous+current),0;
console.log(total) // 15
|
如果我们一步一步来,它看起来像这样:
迭代 | 以前的 | 当前的 | 全部的 |
---|---|---|---|
1 | 0 | 1 | 1 |
2 | 1 | 2 | 3 |
3 | 3 | 3 | 6 |
4 | 6 | 4 | 10 |
5 | 10 | 5 | 15 |
如果您不喜欢表格,请在控制台中运行此代码段:
01
02
03
04
05
06
07
08
09
10
|
const total = [1, 2, 3, 4, 5].reduce(function (previous, current, index) {
const val = previous + current;
console.log(“The previous value is ” + previous +
“; the current value is ” + current +
“, and the current iteration is ” + (index + 1));
return val;
}, 0);
console.log(“The loop is done, and the final value is ” + total + “.”);
|
回顾一下: reduce遍历数组的所有元素,按照您在回调中指定的方式组合它们。在每次迭代中,您的回调都可以访问上 一个 value,即 total-so-far或 累积 value;当前 值;当前 指数;和整个 数组,如果你需要的话。
让我们回到我们的任务示例。我们从 获得了任务名称列表 map,以及经过过滤的任务列表,这些任务需要很长时间……好吧, filter。
如果我们想知道我们今天工作的总时间怎么办?
使用 forEach循环,你会写:
01
02
03
04
05
06
07
08
09
10
11
|
let total_time = 0;
tasks.forEach(function (task) {
// The plus sign just coerces
// task.duration from a String to a Number
total_time += (+task.duration);
});
console.log(total_time)
//expected result is 420
|
, reduce则变为:
1
2
|
total_time = tasks.reduce((previous, current) => previous + current.duration, 0);
console.log(total_time); //420
|
这几乎就是它的全部内容。几乎,因为 JavaScript 为我们提供了另一种鲜为人知的方法,称为 reduceRight. 在上面的示例中, 从数组中的第一reduce项开始,从左到右迭代:
1
2
3
4
5
6
|
let array_of_arrays = [[1, 2], [3, 4], [5, 6]];
const concatenated = array_of_arrays.reduce( function (previous, current) {
return previous.concat(current);
});
console.log(concatenated); // [1, 2, 3, 4, 5, 6];
|
reduceRight做同样的事情,但方向相反:
1
2
3
4
5
6
|
let array_of_arrays = [[1, 2], [3, 4], [5, 6]];
const concatenated = array_of_arrays.reduceRight( function (previous, current) {
return previous.concat(current);
});
console.log(concatenated); // [5, 6, 3, 4, 1, 2];
|
我 reduce每天都在使用,但我从来不需要 reduceRight. 我想你可能也不会。但如果你曾经做过,现在你知道它就在那里。
陷阱
三个大问题 reduce是:
- 忘记 return
- 忘记初始值
- reduce返回单个值时期望数组
幸运的是,前两个很容易避免。决定你的初始值应该是什么取决于你在做什么,但你会很快掌握它的窍门。
最后一个可能看起来有点奇怪。如果 reduce只返回一个值,你为什么会期望一个数组?
有几个很好的理由。首先,reduce 总是返回一个 值,而不是一个 数字。例如,如果您减少一个数组数组,它将返回一个数组。如果您习惯于减少数组,那么可以期望包含单个项目的数组不会是特殊情况。
其次,如果 reduce 确实返回了一个具有单个值的数组,它自然会很好地与 mapand filter以及您可能与它一起使用的数组上的其他函数一起使用。
执行
是我们最后一次深入了解的时候了。像往常一样,如果你想检查一下,Mozilla 有一个用于 reduce 的防弹 polyfill 。
1
2
3
4
5
6
7
8
9
|
let reduce = function (array, callback, initial) {
let accumulator = initial || 0;
array.forEach(function (element) {
accumulator = callback(accumulator, array[i]);
});
return accumulator;
};
|
这里需要注意两点:
- 这一次,我使用名称 accumulator而不是 previous. 这是你通常会在野外看到的。
- 如果用户提供了一个初始值,我会分配accumulator一个初始值,0如果没有,则默认为。这也是真实的 reduce行为方式。
放在一起:映射、过滤、减少和可链接性
在这一点上,你可能不会 那么 印象深刻。很公平: map,filter和reduce, 就其本身而言,并不是很有趣。毕竟,它们真正的力量在于它们的可链接性。
假设我想做以下事情:
- 收集两天的任务。
- 将任务持续时间转换为小时而不是分钟。
- 过滤掉需要两个小时或更长时间的所有内容。
- 总结一下。
- 将结果乘以计费的每小时费率。
- 输出格式化的美元金额。
首先,让我们定义周一和周二的任务:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
const monday = [
{
‘name’ : ‘Write a tutorial’,
‘duration’ : 180
},
{
‘name’ : ‘Some web development’,
‘duration’ : 120
}
];
const tuesday = [
{
‘name’ : ‘Keep writing that tutorial’,
‘duration’ : 240
},
{
‘name’ : ‘Some more web development’,
‘duration’ : 180
},
{
‘name’ : ‘A whole lot of nothing’,
‘duration’ : 240
}
];
const tasks = [monday, tuesday];
|
现在,我们可爱的转变:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
const result = tasks
// Concatenate our 2D array into a single list
.reduce((acc, current) => acc.concat(current))
// Extract the task duration, and convert minutes to hours
.map((task) => task.duration / 60)
// Filter out any task that took less than two hours
.filter((duration) => duration >= 2)
// Multiply each tasks’ duration by our hourly rate
.map((duration) => duration * 25)
// Combine the sums into a single dollar amount
.reduce((acc, current) => [(+acc) + (+current)])
// Convert to a “pretty-printed” dollar amount
.map((amount) => ‘$’ + amount.toFixed(2))
// Pull out the only element of the array we got from map
.reduce((formatted_amount) =>formatted_amount);
|
如果你已经做到了这一步,这应该很简单。不过,有两个奇怪的地方需要解释。
首先,在第 10 行,我必须写:
1
2
3
4
|
// Remainder omitted
reduce(function (accumulator, current) {
return [(+accumulator) + (+current_];
})
|
这里需要说明两点:
- 前面的加号将 accumulator它们 current的值强制转换为数字。如果你不这样做,返回值将是相当无用的字符串, “12510075100”.
- 如果您不将该总和括在括号中, reduce 则会吐出一个值, 而不是 一个数组。那最终会抛出 a TypeError,因为您只能 map 在数组上使用!
可能让你有点不舒服的第二点是最后一点reduce,即:
1
2
3
4
5
6
|
// Remainder omitted
map(function (dollar_amount) {
return ‘$’ + dollar_amount.toFixed(2);
}).reduce(function (formatted_dollar_amount) {
return formatted_dollar_amount;
});
|
该调用 map返回一个包含单个值的数组。在这里,我们调用 reduce以提取该值。
最后,让我们看看我们的朋友 forEach循环如何完成它:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
let concatenated = monday.concat(tuesday),
fees = [],
hourly_rate = 25,
total_fee = 0;
concatenated.forEach(function (task) {
let duration = task.duration / 60;
if (duration >= 2) {
fees.push(duration * hourly_rate);
}
});
fees.forEach(function (fee) {
total_fee += fee;
});
console.log(total_fee); //400
|
可以忍受,但很吵。
结论和后续步骤
在本教程中,您了解了 map、 filter和 reduce工作原理;如何使用它们;以及它们的大致实施方式。您已经看到它们都允许您避免使用 for和 forEach循环所需的变异状态,您现在应该对如何将它们链接在一起有了一个很好的想法。