如何用Reduce将对象数组聚合为指定结构的对象或数组?
问题描述
我有如下对象数组:
const inputArray = [ { name: "sam", date: "1 / 1 / 23", confirmed: "yes", spent: 0 }, { name: "sam", date: "1 / 2 / 23", confirmed: "yes", spent: 4 }, { name: "sam", date: "1 / 3 / 23", confirmed: "yes", spent: 4 }, { name: "sam", date: "1 / 4 / 23", confirmed: "no", spent: 4 }, { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 4 }, { name: "bill", date: "1 / 6 / 23", confirmed: "yes", spent: 4 }, { name: "bill", date: "1 / 5 / 23", confirmed: "yes", spent: 0 }, { name: "annie", date: "1 / 6 / 23", confirmed: "yes", spent: 0 }, { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 }, { name: "annie", date: "1 / 6 / 23", confirmed: "no", spent: 2 }, ];
需要生成满足以下要求的输出对象:
spent字段为对应name的所有spent值之和confirmedAndNotSpent字段为对应name中confirmed为'yes'且spent为0的记录数notConfirmedAndSpent字段为对应name中confirmed为'no'的记录数
期望的对象格式输出如下:
const outputObj = { sam: { spent: 12, confirmedAndNotSpent: 1, notConfirmedAndSpent: 1 }, bill: { spent: 8, confirmedAndNotSpent: 1, notConfirmedAndSpent: 0 }, annie: { spent: 4, confirmedAndNotSpent: 1, notConfirmedAndSpent: 2 }, };
我尝试了以下代码:
let try1 = inputArray.reduce((accumulator, current) => { if (!accumulator[current.name]) accumulator[current.name] = 0; accumulator[current.name] += +current.spent; return accumulator; }, {});
该代码得到输出:{ sam: 12, bill: 8, annie: 4 },但无法扩展为目标结构。尝试的第二段代码:
let try2 = inputArray.reduce((accumulator, current) => { if (!accumulator[current.name]) accumulator[current.name] = {}; accumulator[current.name][current.spent] += +current.spent; return accumulator; }, {})
得到输出:
{ sam: { '0': NaN, '4': NaN }, bill: { '0': NaN, '4': NaN }, annie: { '0': NaN, '2': NaN } }
请问如何实现目标输出?
补充:另一种输出格式
后续还需要生成如下数组格式的输出:
const outputObj2 = [ { name: "sam", spent: 12, confirmedAndNotSpent: 1, notConfirmedAndSpent: 1 }, { name: "bill", spent: 8, confirmedAndNotSpent: 1, notConfirmedAndSpent: 0 }, { name: "annie", spent: 4, confirmedAndNotSpent: 1, notConfirmedAndSpent: 2 }, ];
我尝试了以下代码得到正确输出,但不确定是否为最优实现,希望得到改进建议:
let try3 = inputArray.reduce((accumulator, current) => { if (!accumulator.some((accumulator) => accumulator.name === current.name)) { let obj = { name: current.name, spent: current.spent, confirmedAndNotSpent: current.confirmed === "yes" && current.spent === 0 ? 1 : 0, notConfirmedAndSpent: current.confirmed === "no" ? 1 : 0, }; accumulator.push(obj); } else { //Find index of specific object using findIndex method. objIndex = accumulator.findIndex((obj) => obj.name == current.name); //update the specific object using index accumulator[objIndex].spent += current.spent; if (current.confirmed === "yes" && current.spent === 0) { accumulator[objIndex].confirmedAndNotSpent++; } if (current.confirmed === "no") { accumulator[objIndex].notConfirmedAndSpent++; } } return accumulator; }, []);
解决方案
一、生成对象格式的输出
可以通过reduce方法,为每个用户初始化包含三个目标字段的对象,再遍历每条记录更新对应字段:
const outputObj = inputArray.reduce((acc, curr) => { // 若当前用户不存在,初始化统计对象 if (!acc[curr.name]) { acc[curr.name] = { spent: 0, confirmedAndNotSpent: 0, notConfirmedAndSpent: 0 }; } // 更新总花费 acc[curr.name].spent += curr.spent; // 更新confirmedAndNotSpent计数 if (curr.confirmed === 'yes' && curr.spent === 0) { acc[curr.name].confirmedAndNotSpent++; } // 更新notConfirmedAndSpent计数 if (curr.confirmed === 'no') { acc[curr.name].notConfirmedAndSpent++; } return acc; }, {});
这段代码的逻辑是:
- 遍历数组时,先检查累加器中是否已有当前用户的统计对象,没有则初始化三个字段为0
- 每次遍历都累加
spent值 - 根据当前记录的
confirmed和spent值,判断是否需要增加对应计数字段的值
二、生成数组格式的输出(优化现有实现)
你当前的try3代码虽然能得到正确结果,但每次判断用户是否存在时用some,更新时又用findIndex,这两个方法都会遍历数组,时间复杂度是O(n²),当数据量较大时效率会降低。
优化方案1:先统计再转数组
先用reduce生成对象格式的统计结果,再将对象转换为数组,时间复杂度为O(n),效率更高:
// 先生成对象格式的统计结果 const tempObj = inputArray.reduce((acc, curr) => { if (!acc[curr.name]) { acc[curr.name] = { name: curr.name, spent: 0, confirmedAndNotSpent: 0, notConfirmedAndSpent: 0 }; } acc[curr.name].spent += curr.spent; if (curr.confirmed === 'yes' && curr.spent === 0) { acc[curr.name].confirmedAndNotSpent++; } if (curr.confirmed === 'no') { acc[curr.name].notConfirmedAndSpent++; } return acc; }, {}); // 将对象转换为数组 const outputObj2 = Object.values(tempObj);
优化方案2:一次reduce完成,用映射表避免重复遍历
维护一个映射表(对象)来记录用户在数组中的索引,避免重复遍历数组:
const outputObj2 = inputArray.reduce(({ arr, map }, curr) => { if (!map[curr.name]) { const newItem = { name: curr.name, spent: 0, confirmedAndNotSpent: 0, notConfirmedAndSpent: 0 }; arr.push(newItem); map[curr.name] = arr.length - 1; // 记录新元素的索引 } const index = map[curr.name]; arr[index].spent += curr.spent; if (curr.confirmed === 'yes' && curr.spent === 0) { arr[index].confirmedAndNotSpent++; } if (curr.confirmed === 'no') { arr[index].notConfirmedAndSpent++; } return { arr, map }; }, { arr: [], map: {} }).arr;
这段代码通过map对象存储每个用户对应的数组索引,每次处理记录时直接通过索引找到对应元素更新,避免了some和findIndex的重复遍历,效率更高。
内容的提问来源于stack exchange,提问作者moys
相关产品推荐
相关产品推荐

