javascript Map和Reduce之间的主要区别

nzk0hqpo  于 2023-01-04  发布在  Java
关注(0)|答案(9)|浏览(228)

我使用了这两种方法,但我对这两种方法的用法都很困惑。
是否有map可以做但reduce不能做的事情,反之亦然?
注:我知道如何使用这两种方法,我质疑这些方法之间的主要差异以及何时需要使用。

5cnsuln7

5cnsuln71#


Source
mapreduce的输入都是您定义的数组和函数。它们在某种程度上是互补的:map不能为多个元素的数组返回单个元素,而reduce将始终返回最终更改的累加器。

一米四分一秒

使用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))

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

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

当你可以同时使用两个函数时,你应该选择哪一个呢?试着想象一下代码的样子。对于提供的例子,你可以使用reduce来计算你提到的squares数组:

// 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)

现在,看看这些,显然第二个实现看起来更好,也更短。通常你会选择更简洁的解决方案,在这个例子中是map。当然,你可以用reduce来做,但简单地说,想想哪个会更短,最终会更好。

oyjwcjzk

oyjwcjzk2#

我想这张图可以回答你关于高阶函数之间的区别

628mspwn

628mspwn3#

通常,“Map”意味着将一系列输入转换成一系列 * 等长 * 的输出,而“减少”意味着将一系列输入转换成一系列 * 更少 * 的输出。
人们所指的“Map缩减”通常被解释为“转换,可能是并行的,串行的合并”。
当你“map”的时候,你写的是一个函数,它把xf(x)转换成一个新的值x1;当你“reduce”的时候,你写的是一个函数g(y),它接受数组y并发出数组y1
它们在数据结构方面产生不同的结果。

l7mqbcuq

l7mqbcuq4#

map()函数通过对输入数组中的每个元素传递函数来返回新数组。
这与reduce()不同,reduce()以相同的方式接受数组和函数,但函数接受2输入-累加器和当前值。
所以reduce()可以像map()一样使用,如果你总是把.concat放到函数的下一个输出的累加器上,但是它更常用于减少数组的维数,比如取一个一维数组并返回一个值,或者扁平化一个二维数组等等。

xqk2d5yq

xqk2d5yq5#

下面我们就来逐一了解一下这两位。

Map

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

var arr = [1, 2, 3];

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

console.log(mapped); // it genrate new array

减少

数组对象的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
nwo49xxi

nwo49xxi6#

我认为这是一个非常好的问题,我不能不同意答案,但我有一种感觉,我们完全没有抓住要点。
更抽象地思考mapreduce可以为我们提供很多非常好的见解。
本答案分为3部分:

  • 在Map和减少之间定义和决定(7分钟)
  • 有意使用reduce(8分钟)
  • 桥接标测图并使用探头缩小(5分钟)

Map或减少

共同特征

mapreduce以有意义和一致的方式在不一定是集合的广泛对象上实现。
它们返回一个对周围的算法有用的值,并且它们只关心这个值。
它们的主要作用是传达关于结构的转换或保存的意图。
结构
“结构”指的是一组概念属性,这些属性描述了抽象对象的特征,如无序列表或二维矩阵,以及它们在数据结构中的具体化。
请注意,两者之间可能存在断开:

  • 无序列表可以被存储为数组,其具有由索引键携带的排序的概念;
  • 2D矩阵可以存储为缺少维度(或嵌套)概念的TypedArray。

Map

map是严格的结构保持变换。
在其他类型的对象上实现它以掌握其语义值是有用的:

class A {
    constructor (value) {
        this.value = value
    }

    map (f) { 
        return new A(f(this.value))
    }
}

new A(5).map(x => x * 2); // A { value: 10 }

实现map的对象可以有各种各样的行为,但是它们总是返回与您在使用提供的回调函数转换值时开始时相同类型的对象。
Array.map返回一个长度和顺序与原始值相同的数组。

在回调arity上

因为map保留了结构,所以它被视为安全的操作,但不是每个回调都是一样的。
使用一元回调:map(x => f(x)),则数组的每个值与其他值的存在完全无关。
另一方面,使用其他两个参数会引入耦合,这可能不符合原始结构。
假设删除或重新排序以下数组中的第二项:在Map之前或之后执行该操作将不会产生相同的结果。
与阵列大小耦合:

[6, 3, 12].map((x, _, a) => x/a.length);
// [2, 1, 4]

与订购耦合:

['foo', 'bar', 'baz'].map((x, i) => [i, x]);
// [[0, 'foo'], [1, 'bar'], [2, 'baz']]

与一个特定值耦合:

[1, 5, 3].map((x, _, a) => x/Math.max(...a));
//[ 0.2, 1, 0.6]

与邻居耦合:

const smooth = (x, i, a) => {
    const prev = a[i - 1] ?? x;
    const next = a[i + 1] ?? x;
    const average = (prev + x + next) / 3;
    return Math.round((x + average) / 2);
};

[1, 10, 50, 35, 40, 1].map(smoothh);
// [ 3, 15, 41, 38, 33, 8 ] 

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

const transfrom = (x, i) => x * i;

❌ array.map(transfrom);
⭕ array.map((x, i) => transfrom(x, i));

map中使用变元函数时,这还有其他好处。

❌ ["1", "2", "3"].map(parseInt);
   // [1, NaN, NaN]
⭕ ["1", "2", "3"].map(x => parseInt(x));
   // [1, 2, 3]

减少

reduce设置一个不受其周围结构影响的值。
同样,让我们在一个更简单的对象上实现它:

class A {
    constructor (value) {
        this.value = value
    }

    reduce (f, init) { 
        return init !== undefined
            ? f(init, this.value)
            : this.value
    }
}

new A(5).reduce(); // 5

const concat = (a, b) => a.concat(b);
new A(5).reduce(concat, []); // [ 5 ]

不管你是保留这个值,还是把它放回其他值中,reduce的输出可以是任何形状,它实际上与map相反。

对阵列的影响

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

需要合并

我们如何返回多个没有结构的值?
这是不可能的。为了只返回一个值,我们有两种选择:

  • 将所述值汇总为一个值;
  • 将值移动到不同的结构中。

现在不是更有意义了吗?

需要初始化

如果没有要返回的值怎么办?
如果reduce返回了一个伪值,就无法知道源数组是空的还是包含了这个伪值,所以除非我们提供一个初始值,否则reduce必须抛出。

减速器的真正用途

在下面的代码片段中,您应该能够猜到reducer f的作用:

[a].reduce(f);
[].reduce(f, a);

没有.它没有被调用.

这是一个微不足道的例子:a是我们要返回的单个值,因此不需要f
顺便说一下,这就是为什么我们之前没有在A类中强制使用reducer的原因:因为它只包含一个值。它在数组中是强制的,因为数组可以包含多个值。
因为只有当你有两个或更多的值时才会调用reducer,所以说它的唯一目的是合并它们只是一个小问题。

关于转换值

在可变长度的数组上,期望reducer转换值是危险的,因为正如我们发现的,它可能不会被调用。
当您需要转换值和改变形状时,我建议您在使用reduce之前使用map
无论如何,为了可读性,将这两个关注点分开是一个好主意。

何时不使用减少

因为reduce是实现结构转换的通用工具,所以我建议您在需要返回数组时避免使用它,如果存在另一个更集中的方法来完成您想要的工作。
具体来说,如果您在map中处理嵌套数组时遇到困难,请在考虑reduce之前先考虑flatMapflat

在reduce的核心

递归二元运算

在数组上实现reduce引入了这个反馈循环,其中reducer的第一个参数是前一次迭代的返回值。
不用说,它看起来一点也不像map的回调。

我们可以递归地实现Array.reduce,如下所示:

const reduce = (f, acc, [current, ...rest]) => 
    rest.length == 0
    ? f(acc, current)
    : reduce(f, f(acc, current), rest)

这突出了reducer f的二进制特性,以及它的返回值如何在下一次迭代中变为新的acc
我让你说服自己以下是真的:

reduce(f, a, [b, c, d])

// is equivalent to
f(f(f(a, b), c), d)

// or if you squint a little 
((a ❋ b) ❋ c) ❋ d

这看起来很熟悉:你知道算术运算遵循诸如"结合性"或"交换性"之类的规则。2我想在这里传达的是同样的规则也适用。
reduce可以剥离周围的结构,但是对于变换时,值仍然在代数结构中绑定在一起。
约化器的代数
代数结构超出了这个答案的范围,所以我将只涉及它们是如何相关的。

((a ❋ b) ❋ c) ❋ d

查看上面的表达式,不言而喻,存在将所有值联系在一起的约束:必须知道如何组合它们,就像+必须知道如何组合1 + 2一样,同样重要的是(1 + 2) + 3

最弱的安全结构

确保这一点的一种方法是强制这些值属于同一个集合,在该集合上reducer是"内部"或"闭合"二元运算,也就是说:用缩减器组合来自该组的任何两个值产生属于同一组的值。
在抽象代数中,这被称为magma,你也可以查找半群,它被更多地讨论,与结合性是一样的(不需要花括号),尽管reduce不关心。

不太安全

生活在岩浆中并非绝对必要:我们可以想象可以组合ab但不能组合cb的情况。
函数组合就是这样一个例子。以下函数之一返回一个字符串,该字符串限制了组合它们的顺序:

const a = x => x * 2;
const b = x => x ** 2;
const c = x => x + ' !';

// (a ∘ b) ∘ c
const abc = x => c(b(a(x)));
abc(5); // "100 !"

// (a ∘ c) ∘ b
const acb = x => b(c(a(x)));
acb(5); // NaN

像许多二元运算一样,函数合成可以用作约简器。
了解我们是否处于重新排序或从数组中删除元素可能使reduce中断的情况是很有价值的。
所以,岩浆:不是绝对必要的,但是非常重要。

初始值如何

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

array.reduce(f, init)

// which is really the same as doing
[init, ...array].reduce(f)

// or
((init ❋ a) ❋ b) ❋ c...

我们现在有了一个附加值。没问题。
"没问题"!?我们说过reducer的目的是合并数组值,但init不是一个 * true * 值:这是我们自己强行引入的,应该不会影响reduce的结果。
问题是:
我们应该选择什么样的init才能使f(init, a)init ❋ a返回a
我们需要一个初始值,它的行为就像它不存在一样,我们需要一个中性元素(或"恒等式")。
你可以查unital magmas么半群(与结合律相同),它们是对带有中性元素的岩浆的咒骂。

一些中性元素

你已经知道很多中性元素

numbers.reduce((a, b) => a + b, 0)

numbers.reduce((a, b) => a * b, 1)

booleans.reduce((a, b) => a && b, true)

strings.reduce((a, b) => a.concat(b), "")

arrays.reduce((a, b) => a.concat(b), [])

vec2s.reduce(([u,v], [x,y]) => [u+x,v+y], [0,0])

mat2s.reduce(dot, [[1,0],[0,1]])

你可以对很多种抽象重复这个模式,注意中性元素和计算不需要这么琐碎(extreme example)。

中性元素的艰辛

我们不得不接受这样的事实,即一些约简只可能用于非空数组,并且添加不好的初始化器并不能解决问题。
减少错误的一些例子:

    • 仅部分中性**
numbers.reduce((a, b) => b - a, 0)

// does not work
numbers.reduce((a, b) => a - b, 0)

b减去0得到b,而从0减去b得到-b,我们说只有"右恒等式"是真的。
并非所有非交换运算都缺少对称中性元素,但这是个好兆头。

    • 超出范围**
const min = (a, b) => a < b ? a : b;

// Do you really want to return Infinity?
numbers.reduce(min, Infinity)

对于非空数组,Infinity是唯一不改变reduce输出的初始值,但我们不太可能希望它实际出现在我们的程序中。
中性元素不是我们为了方便而添加的Joker值,它必须是一个允许的值,否则它什么也做不了。

    • 荒谬**

下面的归约依赖于位置,但是添加初始化器自然会将第一个元素移到第二个位置,这需要在归约器中修改索引以保持该行为。
一个17块一个18块一个
如果接受数组不能为空的事实并简化归约器的定义,那就更简洁了。
此外,初始值是退化的:

  • null不是空数组的第一个元素。
  • 空字符串决不是有效的标识符。

没有办法用初始值来保持"第一性"的概念。

结论

代数结构可以帮助我们以更系统的方式思考我们的程序,知道我们正在处理的是哪一个可以准确地预测我们可以从reduce中期待什么,所以我只能建议你去查一查。
∮再向前一步∮
我们已经看到mapreduce在结构上是如何如此不同,但这并不是说它们是两个孤立的事物。
我们可以用reduce来表示map,因为总是可以重新构建我们开始时使用的相同结构。

const map = f => (acc, x) => 
    acc.concat(f(x))
;

const double = x => x * 2;
[1, 2, 3].reduce(map(double), []) // [2, 4, 6]

再往前推一点,就出现了诸如传感器之类的巧妙技术。
我不想详细谈论这些问题,但我希望你们注意到一些与我们以前所说的相呼应的事情。

探头

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

[1, 2, 3, 4].filter(x => x % 2 == 0)
            .map(x => x ** 2)
            .reduce((a, b) => a + b)
// 20

我们迭代了3次,创建了2个中间数据结构。这段代码是声明性的,但效率不高。转换器尝试协调这两个数据结构。
首先介绍一下使用reduce组合函数的实用程序,因为我们不打算使用方法链接:

const composition = (f, g) => x => f(g(x));
const identity = x => x;

const compose = (...functions) => 
    functions.reduce(composition, identity)
;

// compose(a, b, c) is the same as x => a(b(c(x)))

现在注意下面的mapfilter的实现。我们传入这个reducer函数,而不是直接连接。

const map = f => reducer => (acc, x) => 
    reducer(acc, f(x))
;

const filter = f => reducer => (acc, x) => 
    f(x) ? reducer(acc, x) : acc
;

更具体地看这个问题:
reducer => (acc, x) => [...]
在应用回调函数f之后,剩下的函数将reducer作为输入并返回reducer。
这些对称函数就是我们传递给compose的函数:

const pipeline = compose(
    filter(x => x % 2 == 0), 
    map(x => x ** 2)
);

请记住,compose是使用reduce实现的:前面定义的composition函数组合了对称函数。
此操作的输出是相同形状的函数:它需要一个reducer并返回一个reducer,这意味着

  • 我们有一个岩浆。我们可以继续组成变换,只要它们有这个形状。
  • 我们可以通过使用reducer来使用这个链,它将返回一个可以用于reduce的reducer

如果你需要说服力的话,我让你扩展整个事情,如果你这样做,你会注意到变换将方便地从左到右应用,这是compose的相反方向。
好吧,让我们用这个怪人:

const add = (a, b) => a + b;

const reducer = pipeline(add);

const identity = 0;

[1, 2, 3, 4].reduce(reducer, identity); // 20

我们已经将mapfilterreduce等不同的操作组合到一个reduce中,只迭代一次,没有中间数据结构。
这是一个不小的成就!而且这不是一个你仅仅根据语法的简洁性就可以在mapreduce之间做出决定的方案。
还需要注意的是,我们可以完全控制初始值和最终的reducer,我们使用了0add,但是我们也可以使用[]concat(更现实的是push的性能)或者其他任何可以实现类似concat操作的数据结构。

c7rzv4ha

c7rzv4ha7#

要了解map、filter和reduce之间的区别,请记住:
1.这三种方法都适用于数组所以无论何时你想对一个数组进行任何操作,你都要使用这些方法。
1.这三种方法都遵循函数方法,因此原始数组保持不变。原始数组不变,而是返回一个新的数组/值。

  1. Map返回一个新数组,其元素数与原始数组中的元素数相等。因此,如果原始数组有5个元素,则返回的数组也将有5个元素。每当我们想对数组中的每个元素进行更改时,都会使用此方法。您可以记住,ann数组中的每个元素都被Map到输出数组中的某个新值。因此名称map例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var squaredArr = originalArr.map(function(elem){
  return Math.pow(elem,2);
});
//[1,4,9,16]
  1. Filter返回一个新数组,其元素数等于或少于原始数组。它返回数组中通过某些条件的元素。当我们要对原始数组应用过滤器时,使用此方法,因此名称为filter。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var evenArr = originalArr.filter(function(elem){
  return elem%2==0;
})
//[2,4]
  1. Reduce返回单个值,与map/filter不同。因此,每当我们想对数组的所有元素运行操作,但希望使用所有元素得到单个输出时,我们使用reduce。您可以记住,数组的输出减少为单个值,因此命名为reduce。例如,
var originalArr = [1,2,3,4]
//[1,2,3,4]
var sum = originalArr.reduce(function(total,elem){
  return total+elem;
},0)
//10
1rhkuytd

1rhkuytd8#

map函数在每个元素上执行一个给定的函数,而reduce函数则将数组中的元素简化为一个值,下面我将给出两个函数的例子:

// map function
var arr = [1, 2, 3, 4];
var mappedArr = arr.map((element) => { // [10, 20, 30, 40]
return element * 10;

}) 
// reduce function
var arr2 = [1, 2, 3, 4]
var sumOfArr2 = arr2.reduce((total, element) => { // 10
return total + element; 

})
a5g8bdjr

a5g8bdjr9#

reduce确实可以将数组简化为单个值,但是由于我们可以将对象作为initialValue传递,因此我们可以在此基础上进行构建,并最终得到一个比开始时更复杂的对象。比如这个例子,我们根据一些标准对项目进行分组。因此,术语“reduce”对于reduce的能力可能会有轻微的误导,并且认为它必然减少信息可能是错误的,因为它也可能增加信息。

let a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let b = a.reduce((prev, curr) => {
  if (!prev["divisibleBy2"]) {
    prev["divisibleBy2"] = []
  }
  if (curr % 2 === 0) {
    prev["divisibleBy2"].push(curr)
  }
  if (!prev["divisibleBy3"]) {
    prev["divisibleBy3"] = []
  }
  if (curr % 3 === 0) {
    prev["divisibleBy3"].push(curr)
  }
  if (!prev["divisibleBy5"]) {
    prev["divisibleBy5"] = []
  }
  if (curr % 5 === 0) {
    prev["divisibleBy5"].push(curr)
  }

  return prev
}, {})
console.log(b)

相关问题