`map`和`reduce`之间的主要区别

Nis*_*xit 48 javascript reduce dictionary

我使用了这两种方法,但我对这两种方法的使用感到很困惑.

有什么map可以做但不能做reduce,反之亦然?

注意:我知道如何使用这两种方法我正在质疑这些方法之间的主要区别以及何时需要使用.

Ion*_*zău 129

资源

双方mapreduce具有作为输入数组,你定义一个函数.它们在某种程度上是互补的:map不能为多个元素的数组返回一个单独的元素,而reduce总是会返回最终更改的累加器.

map

使用map迭代元素,并为每个元素返回所需的元素.

例如,如果您有一组数字并想要获得它们的方块,则可以执行以下操作:

// A function which calculates the square
const square = x => x * x

// Use `map` to get the square of each number
console.log([1, 2, 3, 4, 5].map(square))
Run Code Online (Sandbox Code Playgroud)

reduce

使用数组作为输入,您可以根据获取accumulatorcurrent_element参数的回调函数(第一个参数)获取一个单独的元素(比如一个Object,或一个Number,或另一个数组):

const numbers = [1, 2, 3, 4, 5]

// Calculate the sum
console.log(numbers.reduce(function (acc, current) {
  return acc + current
}, 0)) // < Start with 0

// Calculate the product
console.log(numbers.reduce(function (acc, current) {
  return acc * current
}, 1)) // < Start with 1
Run Code Online (Sandbox Code Playgroud)


当你可以用两者做同样的事情时,你应该选择哪一个?试着想象一下代码的外观.对于提供的示例,您可以使用以下方法计算正方形数组reduce:

// Using reduce
[1, 2, 3, 4, 5].reduce(function (acc, current) {
    acc.push(current*current);
    return acc;
 }, [])

 // Using map
 [1, 2, 3, 4, 5].map(x => x * x)
Run Code Online (Sandbox Code Playgroud)

现在,看看这些,显然第二个实现看起来更好,而且更短.通常你会选择更清洁的解决方案,在这种情况下map.当然,你可以做到这一点reduce,但简而言之,想想哪个更短,最终会更好.

  • 在选择使用哪个时,意图是关键。如果两者都可以实现相似的结果,并且由于性能差异可以忽略不计,请使用符合您意图的函数,就像 Tadman 在下面提到的“当您“映射”时,您正在编写一个函数,将 x 与 f(x) 转换为一些新的函数值 x1。当你“减少”时,你正在编写一些函数 g(y),它接受数组 y 并发出数组 y1”。 (3认同)
  • 好的,我看到了你的 Map 示例,但我可以用 reduce 函数做同样的事情,哪个是好的,为什么?使用reduce 创建新数组或使用map 修改现有数组。 (2认同)

Yaz*_*jar 31

我想这张图片会回答你关于这些高阶函数之间的区别 在此处输入图片说明

  • 图片来源是什么? (7认同)
  • 我想说这张图片是准确的,除了减少之外,它不会旋转如图所示的正方形 (4认同)
  • 您能想出一种更好的方法来表示信息图的减少吗? (2认同)

geo*_*rey 11

我认为这个问题是一个非常好的问题,我不能不同意答案,但我感觉我们完全没有抓住重点。

\n

map更抽象地思考reduce可以为我们提供很多非常好的见解。

\n

这个答案分为3部分:

\n
    \n
  • 定义并在 Map 和 Reduce 之间做出决定(7 分钟)
  • \n
  • 有意使用reduce(8 分钟)
  • \n
  • 桥接图并使用换能器进行缩减(5 分钟)
  • \n
\n

映射或减少

\n

共同特征

\n

mapreduce以有意义且一致的方式在各种不一定是集合的对象上实现。

\n

它们返回一个对周围算法有用的值,并且它们只关心这个值。

\n

它们的主要作用是传达有关结构改造或保存的意图。

\n

结构

\n

我所说的“结构”是指一组表征抽象对象的概念属性,例如无序列表或二维矩阵,以及它们在数据结构中的具体化。

\n

请注意,两者之间可能存在脱节:

\n
    \n
  • 无序列表可以存储为数组,它具有索引键携带的排序概念;
  • \n
  • 二维矩阵可以存储为 TypedArray,它缺乏维度(或嵌套)的概念。
  • \n
\n

地图

\n
\n

map是严格的结构保持变换。

\n
\n

在其他类型的对象上实现它以掌握其语义价值是有用的:

\n
class A {\n    constructor (value) {\n        this.value = value\n    }\n\n    map (f) { \n        return new A(f(this.value))\n    }\n}\n\nnew A(5).map(x => x * 2); // A { value: 10 }\n
Run Code Online (Sandbox Code Playgroud)\n

实现的对象map可以具有各种行为,但它们总是返回与您在使用提供的回调转换值时开始使用的相同类型的对象。

\n

Array.map返回与原始数组具有相同长度和相同顺序的数组。

\n

关于回调数量

\n

因为它保留了结构,所以map被视为安全操作,但并非每个回调都是相同的。

\n

使用一元回调:map(x => f(x)),数组的每个值与其他值的存在完全无关。

\n

另一方面,使用其他两个参数会引入耦合,这可能不符合原始结构。

\n
\n

想象一下删除或重新排序下面数组中的第二项:在地图之前或之后执行此操作不会产生相同的结果。

\n

与数组大小耦合:

\n
[6, 3, 12].map((x, _, a) => x/a.length);\n// [2, 1, 4]\n
Run Code Online (Sandbox Code Playgroud)\n

与订购耦合:

\n
[\'foo\', \'bar\', \'baz\'].map((x, i) => [i, x]);\n// [[0, \'foo\'], [1, \'bar\'], [2, \'baz\']]\n
Run Code Online (Sandbox Code Playgroud)\n

与一个特定值耦合:

\n
[1, 5, 3].map((x, _, a) => x/Math.max(...a));\n//[ 0.2, 1, 0.6]\n
Run Code Online (Sandbox Code Playgroud)\n

与邻居的耦合:

\n
const smooth = (x, i, a) => {\n    const prev = a[i - 1] ?? x;\n    const next = a[i + 1] ?? x;\n    const average = (prev + x + next) / 3;\n    return Math.round((x + average) / 2);\n};\n\n[1, 10, 50, 35, 40, 1].map(smoothh);\n// [ 3, 15, 41, 38, 33, 8 ]\xe2\x80\x88\n
Run Code Online (Sandbox Code Playgroud)\n
\n

我建议在调用站点上明确说明是否使用这些参数。

\n
const transfrom = (x, i) => x * i;\n\n\xe2\x9d\x8c array.map(transfrom);\n\xe2\xad\x95 array.map((x, i) => transfrom(x, i));\n
Run Code Online (Sandbox Code Playgroud)\n

当您将可变参数函数与map.

\n
\xe2\x9d\x8c ["1", "2", "3"].map(parseInt);\n   // [1, NaN, NaN]\n\xe2\xad\x95 ["1", "2", "3"].map(x => parseInt(x));\n   // [1, 2, 3]\n
Run Code Online (Sandbox Code Playgroud)\n

减少

\n
\n

reduce设置一个不受其周围结构影响的值。

\n
\n

再次,让我们在一个更简单的对象上实现它:

\n
class A {\n    constructor (value) {\n        this.value = value\n    }\n\n    reduce (f, init) { \n        return init !== undefined\n            ? f(init, this.value)\n            : this.value\n    }\n}\n\nnew A(5).reduce(); // 5\n\nconst concat = (a, b) => a.concat(b);\nnew A(5).reduce(concat, []); // [ 5 ]\n
Run Code Online (Sandbox Code Playgroud)\n

无论您保留该值还是将其放回到其他东西中, 的输出都reduce可以是任何形状。它实际上是相反的map

\n

对数组的影响

\n

数组可以包含多个值或零值,这会产生两个有时相互冲突的要求。

\n需要结合\n
\n

我们如何返回多个没有结构的值?

\n
\n

是不可能的。为了只返回一个值,我们有两种选择:

\n
    \n
  • 将这些值汇总为一个值;
  • \n
  • 将值转移到不同的结构中。
  • \n
\n

现在不是更有意义了吗?

\n需要初始化\n
\n

如果没有返回值怎么办?

\n
\n

如果reduce返回一个假值,则无法知道源数组是否为空或者是否包含该假值,因此除非我们提供初始值,否则reduce必须抛出异常。

\n

减速机的真正用途

\n

您应该能够f在以下代码片段中猜出减速器的作用:

\n
[a].reduce(f);\n[].reduce(f, a);\n
Run Code Online (Sandbox Code Playgroud)\n

没有什么。它不被称为。

\n

这是一个简单的情况:a是我们想要返回的单个值,因此f不需要。

\n

顺便说一句,这就是我们之前没有在类中强制使用减速器的原因A:因为它只包含一个值。它对于数组是强制性的,因为数组可以包含多个值。

\n

由于只有当您有 2 个或更多值时才会调用减速器,因此说它的唯一目的是将它们组合起来只是一箭之遥。

\n

论价值观的转变

\n

在可变长度的数组上,期望减速器转换值是危险的,因为正如我们发现的那样,它可能不会被调用。

\n

当你需要转变价值观和改变形态时,我鼓励你map先这样做。reduce

\n

无论如何,为了可读性,将这两个问题分开是个好主意。

\n

何时不使用减少

\n

因为reduce这是用于实现结构转换的通用工具,所以当您想要返回数组时,如果存在另一种更集中的方法可以完成您想要的操作,我建议您避免使用它。

\n

具体来说,如果您在使用 a 中的嵌套数组时遇到困难,请在使用 之前先map考虑flatMap或。flatreduce

\n

减少的核心

\n

递归二元运算

\n

在数组上实现reduce引入了这个反馈循环,其中减速器的第一个参数是前一次迭代的返回值。

\n

不用说,它看起来一点也不像map\ 的回调。

\n

我们可以像这样递归地实现Array.reduce

\n
const reduce = (f, acc, [current, ...rest]) => \n    rest.length == 0\n    ? f(acc, current)\n    : reduce(f, f(acc, current), rest)\n
Run Code Online (Sandbox Code Playgroud)\n

这突出了减速器的二进制性质以及它的返回值如何在下一次迭代中f成为新值。acc

\n

我让你说服自己以下内容是正确的:

\n
reduce(f, a, [b, c, d])\n\n// is equivalent to\nf(f(f(a, b), c), d)\n\n// or if you squint a little \n((a \xe2\x9d\x8b b) \xe2\x9d\x8b c) \xe2\x9d\x8b d\n
Run Code Online (Sandbox Code Playgroud)\n

这看起来应该很熟悉:您知道算术运算遵循“结合性”或“交换性”等规则。我在这里想表达的是,同样的规则也适用。

\n

reduce可能会去掉周围的结构,但在转换时值仍然以代数结构绑定在一起。

\n

减速器代数

\n

代数结构超出了本答案的范围,因此我只会讨论它们的相关性。

\n
((a \xe2\x9d\x8b b) \xe2\x9d\x8b c) \xe2\x9d\x8b d\n
Run Code Online (Sandbox Code Playgroud)\n

看看上面的表达式,不言而喻的是,有一个约束将所有值联系在一起:\xe2\x9d\x8b必须知道如何以相同的方式组合它们+必须知道如何组合1 + 2并且同样重要(1 + 2) + 3

\n

最弱的安全结构

\n

确保这一点的一种方法是强制这些值属于同一集合,在该集合上,减速器是“内部”或“封闭”二元运算,也就是说:将该集合中的任意两个值与减速器相结合会产生一个值属于同一集合。

\n

在抽象代数中,这被称为岩浆。您还可以查找更多讨论的半群,并且与结合性相同(不需要大括号),尽管reduce并不关心。

\n

不太安全

\n

生活在岩浆中并不是绝对必要的:我们可以想象一种情况,其中\xe2\x9d\x8b可以结合ab但不能c结合 和b

\n

函数组合就是一个例子。以下函数之一返回一个字符串,该字符串限制了组合它们的顺序:

\n
const a = x => x * 2;\nconst b = x => x ** 2;\nconst c = x => x + \' !\';\n\n// (a \xe2\x88\x98 b) \xe2\x88\x98 c\nconst abc = x => c(b(a(x)));\nabc(5); // "100 !"\n\n// (a \xe2\x88\x98 c) \xe2\x88\x98 b\nconst acb = x => b(c(a(x)));\nacb(5); // NaN\n
Run Code Online (Sandbox Code Playgroud)\n

与许多二元运算一样,函数组合可以用作化简器。

\n

了解我们是否处于重新排序或从数组中删除元素可能会造成reduce破坏的情况是很有价值的。

\n

所以,岩浆:不是绝对必要的,但非常重要。

\n

初始值怎么样

\n

假设我们想通过引入一个初始值来防止数组为空时引发异常:

\n
array.reduce(f, init)\n\n// which is really the same as doing\n[init, ...array].reduce(f)\n\n// or\n((init \xe2\x9d\x8b a) \xe2\x9d\x8b b) \xe2\x9d\x8b c...\n
Run Code Online (Sandbox Code Playgroud)\n

我们现在有了额外的价值。没问题。

\n

“没问题”!?我们说reducer的目的是组合数组值,但init不是真正的值:它是我们自己强制引入的,它不应该影响 的结果reduce

\n

问题是:

\n
\n

init我们应该选择什么f(init, a)才能init \xe2\x9d\x8b a返回a

\n
\n

我们想要一个初始值,就像它不存在一样。我们想要一个中立的元素(或“身份”)。

\n

您可以查找单位岩浆或幺半群(与结合性相同),它们是配备中性元素的岩浆的脏话。

\n

一些中性元素

\n

你已经知道了一堆中性元素

\n
numbers.reduce((a, b) => a + b, 0)\n\nnumbers.reduce((a, b) => a * b, 1)\n\nbooleans.reduce((a, b) => a && b, true)\n\nstrings.reduce((a, b) => a.concat(b), "")\n\narrays.reduce((a, b) => a.concat(b), [])\n\nvec2s.reduce(([u,v], [x,y]) => [u+x,v+y], [0,0])\n\nmat2s.reduce(dot, [[1,0],[0,1]])\n
Run Code Online (Sandbox Code Playgroud)\n

您可以对多种抽象重复此模式。请注意,中性元素和计算不需要这么简单(极端的例子)。

\n

中性元素苦难

\n
\n

我们必须接受这样一个事实,即某些减少仅适用于非空数组,并且添加不良初始化程序并不能解决问题。

\n
\n

一些减少错误的例子:

\n仅部分中立\n
numbers.reduce((a, b) => b - a, 0)\n\n// does not work\nnumbers.reduce((a, b) => a - b, 0)\n
Run Code Online (Sandbox Code Playgroud)\n

减去0formb返回b,但减去bfrom0返回-b。\n我们说只有“右恒等式”才是正确的。

\n

并非每个非交换运算都缺乏对称中性元素,但这是一个好兆头。

\n超出范围\n
const min = (a, b) => a < b ? a : b;\n\n// Do you really want to return Infinity?\nnumbers.reduce(min, Infinity)\n
Run Code Online (Sandbox Code Playgroud)\n

Infinity是唯一一个不会改变reduce非空数组输出的初始值,但我们不太可能希望它实际出现在我们的程序中。

\n

中性元素并不是我们为了方便而添加的小丑值。它必须是一个允许的值,否则它不会完成任何事情。

\n无意义\n

下面的减少依赖于位置,但是添加初始化程序自然会将第一个元素移动到第二个位置,这需要弄乱减速器中的索引来维持行为。

\n
const first = (a, b, i) => !i ? b : a;\nthings.reduce(first, null);\n
Run Code Online (Sandbox Code Playgroud)\n
const camelCase = (a, b, i) =>  a + (\n    !i ? b : b[0].toUpperCase() + b.slice(1)\n);\nwords.reduce(camelCase, \'\');\n
Run Code Online (Sandbox Code Playgroud)\n

接受数组不能为空的事实并简化化简器的定义会更清晰。

\n

此外,初始值是退化的:

\n
    \n
  • null不是空数组的第一个元素。

    \n
  • \n
  • 空字符串绝不是有效的标识符。

    \n
  • \n
\n

没有办法用初始值来保存“第一性”的概念。

\n

结论

\n

代数结构可以帮助我们以更系统的方式思考我们的程序。知道我们正在处理的是哪一个可以准确预测我们可以期待什么reduce,所以我只能建议你查一下它们。

\n

更进一步

\n

我们已经看到mapreduce在结构上如此不同,但这并不是说它们是两个孤立的事物。

\n

我们可以map用 来表达reduce,因为总是可以重建我们开始时使用的相同结构。

\n
const map = f => (acc, x) => \n    acc.concat(f(x))\n;\n\nconst double = x => x * 2;\n[1, 2, 3].reduce(map(double), []) // [2, 4, 6]\n
Run Code Online (Sandbox Code Playgroud)\n

进一步推动它已经产生了诸如传感器之类的巧妙技巧。

\n

我不会详细介绍它们,但我希望您注意一些与我们之前所说的相呼应的事情。

\n

传感器

\n

首先让我们看看我们要解决什么问题

\n
[1, 2, 3, 4].filter(x => x % 2 == 0)\n            .map(x => x ** 2)\n            .reduce((a, b) => a + b)\n// 20\n
Run Code Online (Sandbox Code Playgroud)\n

我们迭代 3 次并创建 2 个中间数据结构。这段代码是声明性的,但效率不高。传感器试图协调两者。

\n

首先是使用 来编写函数的一些实用工具reduce,因为我们不会使用方法链:

\n
const composition = (f, g) => x => f(g(x));\nconst identity = x => x;\n\nconst compose = (...functions) => \n    functions.reduce(composition, identity)\n;\n\n// compose(a, b, c) is the same as x => a(b(c(x)))\n
Run Code Online (Sandbox Code Playgroud)\n

现在注意下面的map执行filter。我们传入这个reducer函数而不是直接连接。

\n
const map = f => reducer => (acc, x) => \n    reducer(acc, f(x))\n;\n\nconst filter = f => reducer => (acc, x) => \n    f(x) ? reducer(acc, x) : acc\n;\n
Run Code Online (Sandbox Code Playgroud)\n

更具体地看一下:
\n reducer => (acc, x) => [...]
\n应用回调函数后f,我们剩下一个函数,该函数接受一个reducer作为输入并返回一个reducer。

\n

这些对称函数是我们传递给的compose

\n
const pipeline = compose(\n    filter(x => x % 2 == 0), \n    map(x => x ** 2)\n);\n
Run Code Online (Sandbox Code Playgroud)\n

记住compose是用 实现的reduce:我们composition之前定义的函数结合了我们的对称函数。

\n

此操作的输出是相同形状的函数:需要一个减速器并返回一个减速器,这意味着

\n
    \n
  • 我们有岩浆。只要它们具有这种形状,我们就可以继续组合变换。
  • \n
  • 我们可以通过将结果函数与reducer一起应用来使用这个链,这将返回一个我们可以使用的reducerreduce
  • \n
\n

如果你需要说服力,我可以让你扩展整个事情。如果这样做,您会注意到变换将方便地从左到右应用,这与 的方向相反compose

\n

好吧,让我们使用这个奇怪的东西:

\n
const add = (a, b) => a + b;\n\nconst reducer = pipeline(add);\n\nconst identity = 0;\n\n[1, 2, 3, 4].reduce(reducer, identity); // 20\n
Run Code Online (Sandbox Code Playgroud)\n

map我们将、filter和等多种操作组合reduce成一个单一的reduce,仅迭代一次,没有中间数据结构。

\n

这是一个不小的成就!map而且这并不是一个仅仅reduce根据语法的简洁性就可以想出的方案。

\n

另请注意,我们可以完全控制初始值和最终的减速器。我们使用了0and add,但我们可以使用[]and concat(更实际的push性能方面)或任何其他可以实现类似连接操作的数据结构。

\n


tad*_*man 10

通常,“映射”是指将一系列输入转换为等长的一系列输出,而“减少”是指将一系列输入转换为少量的输出。

人们所说的“ map-reduce”通常被理解为“变换,可能并行,串行组合”。

当“地图”,你写一个函数变换xf(x)进入一些新的价值x1。当您“减少”时,您正在编写一些g(y)接受array y并发出array的函数y1。他们处理不同类型的数据并产生不同的结果。


Joe*_*don 7

map()函数通过在输入数组中的每个元素上传递一个函数来返回一个新数组。

这与以reduce()相同方式接受数组和函数的方式不同,但该函数接受2输入 - 累加器和当前值。

所以reduce()可以像map()如果你总是.concat在累加器上使用函数的下一个输出。然而,它更常用于减少数组的维数,因此要么取一维并返回单个值,要么展平二维数组等。


pat*_*pan 6

让我们一一看看这两个。

地图

Map 接受一个回调并对数组中的每个元素运行它,但它的独特之处在于它根据您现有的数组生成一个新数组

var arr = [1, 2, 3];

var mapped = arr.map(function(elem) {
    return elem * 10;
})

console.log(mapped); // it genrate new array
Run Code Online (Sandbox Code Playgroud)

降低

数组对象的 Reduce 方法用于将数组缩减为一个值

var arr = [1, 2, 3];

var sum = arr.reduce(function(sum, elem){
    return sum + elem;
})

console.log(sum) // reduce the array to one single value
Run Code Online (Sandbox Code Playgroud)