停止在 map、reduce 和 forEach Map forEach Reduce 中进行变异

2025-06-07

停止在 map、reduce 和 forEach 中进行变异

地图

forEach

减少

有很多文章试图说服你应该使用mapfilterreduce方法。但很少有文章提到forEach,而且很少有文章提到更传统的 for 循环作为可靠的替代方案。或者,何时使用mapreduce,尤其是forEach

编程主要关乎观点,以及(或许有点夸张了)我们喜欢称之为“常识”的东西。在本文中,我将分享我的观点,并探讨函数和副作用(变异)的问题。这篇文章的灵感源于Erik Rasmussen 今天发的一条推文,以及过去的经验。

我还记得我在代码审查期间提出的这个变更请求。它在团队中逐渐传播开来,甚至在下一次回顾会议中也进行了讨论。PR #1069,2019 年 7 月 18 日,作者不重要



path?.map(id => checkID(id)); // eslint-disable-line no-unused-expressions


Enter fullscreen mode Exit fullscreen mode

我的要求是将其更改为:



path?.forEach(id => checkID(id));


Enter fullscreen mode Exit fullscreen mode

简单介绍一下背景知识,path它是一个string[],它checkID会对该字符串进行一些验证,看看它是否是一个类似 id 的值。如果不是,它会抛出一个错误。

为什么我要提出变更请求,又为什么要在回顾中提及它?并没有法律禁止在 map 函数中调用方法,也没有法律禁止在 map 函数中抛出异常。只是它不符合我的预期。而且我仍然相信我有权这样做。

地图

我的期望map是它将一个值“映射”到另一个值。像这样:



const input = [1, 2, 3];
const output = input.map(value => value * 2);


Enter fullscreen mode Exit fullscreen mode

有一个输入值([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);


Enter fullscreen mode Exit fullscreen mode

期望

现在让我们以 Erik 的例子为例



return items.map((item) => { 
  item.userId = userId; 
  return item; 
});


Enter fullscreen mode Exit fullscreen mode

并围绕此构建一些代码,以便更容易使用。



function addUserId(userId) {
  return (item) => { 
    item.userId = userId; 
    return item; 
  }
}

const items = [
  { id: 1 },
  { id: 2 },
];

const newItems = items.map(addUserId('abc'));


Enter fullscreen mode Exit fullscreen mode

你现在对修改里面的 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'));


Enter fullscreen mode Exit fullscreen mode

这和你的预期有什么关系?有什么改变吗?

每当我看到forEach,我都会期待副作用。数组中的每个值都会被执行一些操作。 forEach 没有返回值这一事实强化了这种感觉。

这完全是我个人的想法,但我也停止使用函数式 forEach 调用来改变对象了。我仍然可以使用 a,forEach(sideEffect)但我不会用它来改变值。我使用for of循环来做这件事,因为我发现循环更容易识别出它们导致了改变。



const items = [{ id: 1 }, { id: 2 }];

for (const item of items) {
  item.userId = userId;
}

return items;


Enter fullscreen mode Exit fullscreen mode

请将其与原文进行比较,并在评论中分享您的想法:



const items = [{ id: 1 }, { id: 2 }];

const newItems = items.map((item) => {
  item.userId = userId;
  return item;
});

return newItems;


Enter fullscreen mode Exit fullscreen mode

减少

有人会说 Reducereduce是用来改变值的。在我看来,他们错了。Reduce 是用于容器形状变化的情况。想想对象和数组之间的转换,甚至是集合和原始类型的转换。或者数组长度的变化。Reduce 更多的是改变整个集合的形状,而不是改变单个元素的形状。为此,我们有map……

我对这一部分做了一些修改,因此请允许我引用以下评论中Sebastian Larrieu 的话:

Reduce 是将一个集合转换为单个值,这就是为什么它的参数被称为累加器。

Sebastian 很好地总结了 reduce 的用途。想象一下计算一个数字数组的和。输入一个数字数组,输出一个数字。



[1, 2, 3, 4, 5].reduce((sum, value) => sum + value, 0);


Enter fullscreen mode Exit fullscreen mode

但返回值并不总是必须是原始值。例如,分组是 reduce 的另一个非常有效的用例:



[1, 2, 3, 4, 5].reduce((groups, value) => {
  const group = value % 2 ? 'odd' : 'even';
  groups[group].push(value);
  return groups;
}, { even: [], odd: [] });


Enter fullscreen mode Exit fullscreen mode

直到最近(大概两天前),我才发现 reduce 的另一个用途。我用它来替代filter » mapcall,因为reduce它可以在一次迭代中完成相同的操作。想想:



[1, 2, 3, 4, 5]
  .filter(value => value > 3)
  .map(value => value * 2);


Enter fullscreen mode Exit fullscreen mode

或者



[1, 2, 3, 4, 5].reduce((values, value) => {
  if (value <= 3) {
    return values;
  }

  values.push(value * 2)
  return values;
}, []);


Enter fullscreen mode Exit fullscreen mode

这里的区别在于,reduce只会遍历数组一次,而filtermap组合则会遍历数组两次。对于 5 个条目来说,这没什么大不了的。对于更大的列表, 它可能这也没什么大不了的。(我以为是,但我错了

filter().map()更容易阅读。我让代码更难读,却没有任何好处。这样一来,我们又回到了“常识”的问题。编程并非非黑即白。我们不可能把每一条规则或选择都记录下来、规范化或 lint 起来。选择感觉最好的,然后花时间考虑其他方案。


👋 我是 Stephan,正在开发updrafts.app。如果你想了解更多我不太受欢迎的观点,请在Twitter上关注我。

文章来源:https://dev.to/smeijer/stop-mutating-in-map-reduce-and-foreach-58bf
PREV
Typescript 类型断言
NEXT
我为什么退出我的第一家创业公司