停止在 map、reduce 和 forEach 中进行变异
地图
forEach
减少
有很多文章试图说服你应该使用map
、filter
和reduce
方法。但很少有文章提到forEach
,而且很少有文章提到更传统的 for 循环作为可靠的替代方案。或者,何时使用map
或reduce
,尤其是forEach
。
编程主要关乎观点,以及(或许有点夸张了)我们喜欢称之为“常识”的东西。在本文中,我将分享我的观点,并探讨函数和副作用(变异)的问题。这篇文章的灵感源于Erik Rasmussen 今天发的一条推文,以及过去的经验。
我还记得我在代码审查期间提出的这个变更请求。它在团队中逐渐传播开来,甚至在下一次回顾会议中也进行了讨论。PR #1069,2019 年 7 月 18 日,作者不重要。
path?.map(id => checkID(id)); // eslint-disable-line no-unused-expressions
我的要求是将其更改为:
path?.forEach(id => checkID(id));
简单介绍一下背景知识,path
它是一个string[]
,它checkID
会对该字符串进行一些验证,看看它是否是一个类似 id 的值。如果不是,它会抛出一个错误。
为什么我要提出变更请求,又为什么要在回顾中提及它?并没有法律禁止在 map 函数中调用方法,也没有法律禁止在 map 函数中抛出异常。只是它不符合我的预期。而且我仍然相信我有权这样做。
地图
我的期望map
是它将一个值“映射”到另一个值。像这样:
const input = [1, 2, 3];
const output = input.map(value => value * 2);
有一个输入值([1, 2, 3]
),map 对其进行了一些处理,并返回一个全新的值。input !== output
我的预期是,每当数组值发生变化时,它也不会与之前的值匹配。换句话说,我期望至少对于一个元素而言是这样input[n] !== output[n]
。
我们还可以提取回调函数,这样最终得到的就是一个纯粹的、可测试的函数。我对map
回调函数的期望始终是它没有副作用,没有异常。
function double(value) {
return value * 2;
}
const input = [1, 2, 3];
const output = input.map(double);
期望
现在让我们以 Erik 的例子为例
return items.map((item) => {
item.userId = userId;
return item;
});
并围绕此构建一些代码,以便更容易使用。
function addUserId(userId) {
return (item) => {
item.userId = userId;
return item;
}
}
const items = [
{ id: 1 },
{ id: 2 },
];
const newItems = items.map(addUserId('abc'));
你现在对修改里面的 item 对象有什么看法map
?当你看到 Erik 的那段代码时,你可能觉得还行。但提取那个回调函数之后,我希望你开始觉得它不对劲。如果你还没看出我试图强调的问题,请尝试回答以下问题:
- 看起来像什么
items[0]
? - 看起来像什么
newItems[0]
? - 返回什么
items === newItems
? - 返回什么
items[0] === newItems[0]
? - 这些答案符合你的预期吗?
forEach
现在让我们简单地将该 map 调用更改为forEach
。
const items = [
{ id: 1 },
{ id: 2 },
];
items.forEach(addUserId('#abc'));
这和你的预期有什么关系?有什么改变吗?
每当我看到forEach
,我都会期待副作用。数组中的每个值都会被执行一些操作。 forEach 没有返回值这一事实强化了这种感觉。
这完全是我个人的想法,但我也停止使用函数式 forEach 调用来改变对象了。我仍然可以使用 a,forEach(sideEffect)
但我不会用它来改变值。我使用for of
循环来做这件事,因为我发现循环更容易识别出它们导致了改变。
const items = [{ id: 1 }, { id: 2 }];
for (const item of items) {
item.userId = userId;
}
return items;
请将其与原文进行比较,并在评论中分享您的想法:
const items = [{ id: 1 }, { id: 2 }];
const newItems = items.map((item) => {
item.userId = userId;
return item;
});
return newItems;
减少
有人会说 Reducereduce
是用来改变值的。在我看来,他们错了。Reduce 是用于容器形状变化的情况。想想对象和数组之间的转换,甚至是集合和原始类型的转换。或者数组长度的变化。Reduce 更多的是改变整个集合的形状,而不是改变单个元素的形状。为此,我们有map
……
我对这一部分做了一些修改,因此请允许我引用以下评论中Sebastian Larrieu 的话:
Reduce 是将一个集合转换为单个值,这就是为什么它的参数被称为累加器。
Sebastian 很好地总结了 reduce 的用途。想象一下计算一个数字数组的和。输入一个数字数组,输出一个数字。
[1, 2, 3, 4, 5].reduce((sum, value) => sum + value, 0);
但返回值并不总是必须是原始值。例如,分组是 reduce 的另一个非常有效的用例:
[1, 2, 3, 4, 5].reduce((groups, value) => {
const group = value % 2 ? 'odd' : 'even';
groups[group].push(value);
return groups;
}, { even: [], odd: [] });
直到最近(大概两天前),我才发现 reduce 的另一个用途。我用它来替代filter » map
call,因为reduce
它可以在一次迭代中完成相同的操作。想想:
[1, 2, 3, 4, 5]
.filter(value => value > 3)
.map(value => value * 2);
或者
[1, 2, 3, 4, 5].reduce((values, value) => {
if (value <= 3) {
return values;
}
values.push(value * 2)
return values;
}, []);
这里的区别在于,reduce
只会遍历数组一次,而filter
和map
组合则会遍历数组两次。对于 5 个条目来说,这没什么大不了的。对于更大的列表,
它可能这也没什么大不了的。(我以为是,但我错了。)
这filter().map()
更容易阅读。我让代码更难读,却没有任何好处。这样一来,我们又回到了“常识”的问题。编程并非非黑即白。我们不可能把每一条规则或选择都记录下来、规范化或 lint 起来。选择感觉最好的,然后花时间考虑其他方案。
👋 我是 Stephan,正在开发updrafts.app。如果你想了解更多我不太受欢迎的观点,请在Twitter上关注我。
文章来源:https://dev.to/smeijer/stop-mutating-in-map-reduce-and-foreach-58bf