如何在对象数组上调用reduce来总结它们的属性?

IT技术 javascript functional-programming reduce
2021-02-07 02:59:59

假设我想对a.x中的每个元素求和arr

arr = [ { x: 1 }, { x: 2 }, { x: 4 } ];
arr.reduce(function(a, b){ return a.x + b.x; }); // => NaN

我有理由相信这a.xundefined在某个时候。

以下工作正常

arr = [ 1, 2, 4 ];
arr.reduce(function(a, b){ return a + b; }); // => 7

我在第一个例子中做错了什么?

6个回答

经过第一次迭代your're返回一个数字,然后试图获得财产x的它添加到下一个对象,它是undefined和涉及数学undefined的结果NaN

尝试返回一个包含x参数的 x 属性总和属性的对象

var arr = [{x:1},{x:2},{x:4}];

arr.reduce(function (a, b) {
  return {x: a.x + b.x}; // returns object with property x
})

// ES6
arr.reduce((a, b) => ({x: a.x + b.x}));

// -> {x: 7}

从评论中添加的解释:

每次迭代的返回值[].reduce用作a下一次迭代变量。

迭代 1: a = {x:1}, b = {x:2},在迭代 2 中{x: 3}分配给a

迭代 2: a = {x:3}, b = {x:4}.

您的示例的问题在于您正在返回一个数字文字。

function (a, b) {
  return a.x + b.x; // returns number literal
}

迭代 1: a = {x:1}, b = {x:2},// returns 3a下一次迭代一样

迭代 2: a = 3,b = {x:2}返回NaN

许多文字3不(通常)有一个叫做财产x所以它undefinedundefined + b.x回报NaN,并NaN + <anything>始终NaN

澄清:我更喜欢我的方法而不是这个线程中的另一个最佳答案,因为我不同意传递可选参数以使用幻数减少以获取数字原语更清晰的想法。它可能会导致写入的行数减少,但 imo 可读性较差。

实际上,只要阅读stackoverflow.com/questions/2118123/...并了解它是如何工作的......但是当你有一个方法时你会怎么做,而不仅仅是直接的属性?
2021-03-16 02:59:59
在这种情况下,累加器参数需要具有预期的x键才能工作。
2021-03-16 02:59:59
当然,reduce 接受一个函数来对数组中的每个元素执行操作。每次它返回一个值,该值用作操作中的下一个“a”变量。所以第一次迭代 a = {x:1}, b = {x:2} 然后第二次迭代 a = {x:3}(第一次迭代的组合值),b = {x:4}。您的示例在第二次迭代中的问题是它试图添加 3.x + bx, 3 没有名为 x 的属性,因此它返回 undefined 并将其添加到 bx (4) 返回 Not a Number
2021-03-23 02:59:59
我理解解释,但我仍然没有看到 {x: ... } 如何阻止迭代 2 调用 ax?这个 x 是什么意思?我尝试将这种方法与一种方法一起使用,但似乎不起作用
2021-04-07 02:59:59
此处提供的 ES6 解决方案不起作用。请参阅@Casey Chu 的答案以获得有效的解决方案和清晰的解释
2021-04-07 02:59:59

一个更简洁的方法是提供一个初始值作为第二个参数reduce

var arr = [{x:1}, {x:2}, {x:4}];
var result = arr.reduce(function (acc, obj) { return acc + obj.x; }, 0);
console.log(result);  // 7

第一次调用匿名函数时,它被调用(0, {x: 1})并返回0 + 1 = 1下一次,它被调用(1, {x: 2})并返回1 + 2 = 3然后调用 with (3, {x: 4}),最后返回7

这也处理数组为空的情况,返回0.

如果您有初始值(reduce 的第二个参数),这就是解决方案
2021-03-12 02:59:59
这帮助我处理数组长度有时仅为 1 的情况。
2021-03-16 02:59:59
es6 方式 var arr = [{x:1}, {x:2}, {x:4}]; 让 a =arr.reduce( (acc, obj) => acc + obj.x, 0); 控制台日志(一);
2021-03-16 02:59:59
这比接受的答案更少代码,更直接 - 有什么方法可以让接受的答案比这更好?
2021-04-05 02:59:59
谢谢,我正在寻找带有第二个参数的提示reduce.
2021-04-09 02:59:59

TL;DR,设置初始值

使用解构

arr.reduce( ( sum, { x } ) => sum + x , 0)

无解构

arr.reduce( ( sum , cur ) => sum + cur.x , 0)

使用typescript

arr.reduce( ( sum, { x } : { x: number } ) => sum + x , 0)

让我们试试解构方法:

const arr = [ { x: 1 }, { x: 2 }, { x: 4 } ]
const result = arr.reduce( ( sum, { x } ) => sum + x , 0)
console.log( result ) // 7

关键是设置初始值。返回值成为下一次迭代的第一个参数。

最佳答案中使用的技术不是惯用的

接受的答案建议不传递“可选”值。这是错误的,因为惯用的方式是始终包含第二个参数为什么?三个原因:

1. 危险 ——不传入初始值是危险的,如果回调函数不小心会产生副作用和突变。

看哪

const badCallback = (a,i) => Object.assign(a,i) 

const foo = [ { a: 1 }, { b: 2 }, { c: 3 } ]
const bar = foo.reduce( badCallback )  // bad use of Object.assign
// Look, we've tampered with the original array
foo //  [ { a: 1, b: 2, c: 3 }, { b: 2 }, { c: 3 } ]

但是,如果我们这样做了,使用初始值:

const bar = foo.reduce( badCallback, {})
// foo is still OK
foo // { a: 1, b: 2, c: 3 }

作为记录,除非您打算改变原始对象,否则将 的第一个参数设置Object.assign为空对象。像这样:Object.assign({}, a, b, c)

2 - 更好的类型推断 ——当使用像 Typescript 这样的工具或像 VS Code 这样的编辑器时,你会得到告诉编译器初始值的好处,如果你做错了,它可以捕获错误。如果您不设置初始值,在许多情况下它可能无法猜测,最终可能会出现令人毛骨悚然的运行时错误。

3 - 尊重 函子——当 JavaScript 的内部函数子被释放时,它的表现最好。在函数式世界中,有一个关于如何“折叠”或reduce数组的标准。当您对数组折叠或应用catamorphism 时,您可以使用该数组的值来构造一个新类型。您需要传达结果类型——即使最终类型是数组、另一个数组或任何其他类型中的值,您也应该这样做。

让我们换一种方式思考。在 JavaScript 中,函数可以像数据一样传递,这就是回调的工作原理,以下代码的结果是什么?

[1,2,3].reduce(callback)

它会返回一个数字吗?一个东西?这样更清楚

[1,2,3].reduce(callback,0)

在此处阅读有关函数式编程规范的更多信息:https : //github.com/fantasyland/fantasy-land#foldable

更多背景

reduce方法有两个参数,

Array.prototype.reduce( callback, initialItem )

callback函数采用以下参数

(accumulator, itemInArray, indexInArray, entireArray) => { /* do stuff */ }

对于第一次迭代,

  • 如果initialItem提供,则reduce函数将 传递initialItem为 ,accumulator并将数组的第一项传递itemInArray

  • 如果initialItem提供时,reduce功能传递数组作为中的第一项initialItem和所述阵列中的第二项itemInArray可以被混淆的行为。

我教导并建议始终设置reduce的初始值。

您可以在以下位置查看文档:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

希望这可以帮助!

解构正是我想要的。奇迹般有效。谢谢!
2021-03-18 02:59:59

其他人已经回答了这个问题,但我想我会采用另一种方法。您可以组合映射(从 ax 到 x)和减少(添加 x),而不是直接对 ax 求和:

arr = [{x:1},{x:2},{x:4}]
arr.map(function(a) {return a.x;})
   .reduce(function(a,b) {return a + b;});

诚然,它可能会稍微慢一点,但我认为作为一种选择值得一提。

为了形式化已经指出的内容,reducer 是一种 catamorphism,它接受两个可能是相同类型的参数,并返回与第一个参数匹配的类型。

function reducer (accumulator: X, currentValue: Y): X { }

这意味着 reducer 的主体需要将currentValue的当前值转换accumulator为 new 的值accumulator

这在添加时以一种直接的方式工作,因为累加器和元素值都恰好是相同的类型(但用于不同的目的)。

[1, 2, 3].reduce((x, y) => x + y);

这只是有效,因为它们都是数字。

[{ age: 5 }, { age: 2 }, { age: 8 }]
  .reduce((total, thing) => total + thing.age, 0);

现在我们给聚合器一个起始值。在绝大多数情况下,起始值应该是您期望聚合器的类型(您期望作为最终值出现的类型)。虽然您不是被迫(也不应该)这样做,但记住这一点很重要。

一旦你知道了这一点,你就可以为其他 n:1 关系问题写出有意义的归约。

去除重复词:

const skipIfAlreadyFound = (words, word) => words.includes(word)
    ? words
    : words.concat(word);

const deduplicatedWords = aBunchOfWords.reduce(skipIfAlreadyFound, []);

提供找到的所有单词的计数:

const incrementWordCount = (counts, word) => {
  counts[word] = (counts[word] || 0) + 1;
  return counts;
};
const wordCounts = words.reduce(incrementWordCount, { });

将数组数组缩减为单个平面数组:

const concat = (a, b) => a.concat(b);

const numbers = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
].reduce(concat, []);

任何时候您希望从一组事物转到与 1:1 不匹配的单个值,reduce 是您可能会考虑的事情。

事实上,map 和 filter 都可以实现为reductions:

const map = (transform, array) =>
  array.reduce((list, el) => list.concat(transform(el)), []);

const filter = (predicate, array) => array.reduce(
  (list, el) => predicate(el) ? list.concat(el) : list,
  []
);

我希望这为如何使用reduce.

除此之外,我还没有分解的一个补充是,当期望输入和输出类型专门是动态的时,因为数组元素是函数:

const compose = (...fns) => x =>
  fns.reduceRight((x, f) => f(x), x);

const hgfx = h(g(f(x)));
const hgf = compose(h, g, f);
const hgfy = hgf(y);
const hgfz = hgf(z);
+1 用于在类型系统方面清楚地解释它。OTOH,我认为首先提出它是一种变形的想法可能会让许多人反感......
2021-03-29 02:59:59