在前端开发中,我们常常会遇到一个场景:后端返回的数据结构非常"扁平"和"原始",而前端的图表库(如 ECharts)却需要一个结构清晰、高度组织化的数据格式。如何高效、优雅地完成这次数据"整形"(Data Transformation),是衡量前端工程师代码质量的重要标准。
今天,我们就通过一个真实的季度养护记录图表需求,来深入剖析 JavaScript 中两个强大的数组方法:reduce 和 flatMap。
需求分析:从"行"到"列"的转变
首先,我们来看看后端返回的数据是什么样的。这是一个典型的数组对象(Array of Objects)格式,每一行代表一个特定任务在某个季度的数量。
// 后端返回的原始数据 (rawData)
const rawData = [
{ year: 2026, quarter: 1, taskType: "传感器校验", count: 1 },
{ year: 2026, quarter: 1, taskType: "管道清洗", count: 1 },
{ year: 2026, quarter: 1, taskType: "设备润滑", count: 5 },
{ year: 2026, quarter: 2, taskType: "传感器校验", count: 1 },
{ year: 2026, quarter: 2, taskType: "管道清洗", count: 1 },
{ year: 2026, quarter: 2, taskType: "设备润滑", count: 2 }
];
然而,ECharts 的多条折线图需要的是另一种格式:x 轴代表时间,而每一种任务类型(taskType)都成为对象的一个属性(即一"列")。
// ECharts 需要的目标数据格式
const targetData = [
{ time: '2026-Q1', "管道清洗": 1, "设备润滑": 5, "传感器校验": 1 },
{ time: '2026-Q2', "管道清洗": 1, "设备润滑": 2, "传感器校验": 1 }
];
我们的任务就是编写一个函数,将 rawData 转换为 targetData。这个过程可以分为两步:
- 分组与聚合 :将原始数据按照
year-quarter进行分组,并将不同类型的任务数量聚合到同一个对象中。 - 提取图例:从聚合后的数据中,提取出所有的任务类型,用于配置图表的图例(legend)和系列(series)。
️ 第一步:使用 reduce 进行数据分组与聚合
reduce 方法是 JavaScript 中最强大的数组方法之一,它可以将数组"缩减"为任何一个你想要的值------一个数字、一个字符串,或者像我们这里需要的,一个对象。
它的核心思想是:遍历数组的每一项,并通过一个"累加器"(accumulator)来累积结果。
const transformQuarterData = (rawData) => {
// 1. 按 year+quarter 分组
const grouped = rawData.reduce((acc, item) => {
// 生成时间键,如 '2026-Q1'
const time = `${item.year}-Q${item.quarter}`;
// 如果累加器中还没有这个时间键,就初始化它
// 注意这里我们创建了一个以 time 为属性的对象 { time: '2026-Q1' }
if (!acc[time]) {
acc[time] = { time };
}
// 将当前项的任务类型和数量添加到对应的对象中
// 例如:acc['2026-Q1']['管道清洗'] = 1
acc[time][item.taskType] = item.count;
// 返回累加器,供下一次迭代使用
return acc;
}, {}); // 第二个参数 '{}' 是累加器的初始值,一个空对象
// 2. 将聚合后的对象转为数组
// Object.values() 会取出对象的所有值,组成一个新数组
return Object.values(grouped);
};
reduce 执行过程详解:
| 迭代次数 | item (当前项) |
acc (累加器,迭代后) |
|---|---|---|
| 初始值 | - | {} |
| 1 | {..., taskType: "传感器校验", ...} |
{ '2026-Q1': { time: '2026-Q1', "传感器校验": 1 } } |
| 2 | {..., taskType: "管道清洗", ...} |
{ '2026-Q1': { time: '2026-Q1', "传感器校验": 1, "管道清洗": 1 } } |
| 3 | {..., taskType: "设备润滑", ...} |
{ '2026-Q1': { time: '2026-Q1', "传感器校验": 1, "管道清洗": 1, "设备润滑": 5 } } |
| 4 | {..., taskType: "传感器校验", ...} |
{ '2026-Q1': {...}, '2026-Q2': { time: '2026-Q2', "传感器校验": 1 } } |
| ... | ... | ... |
最终,grouped 对象的结构如下:
{
'2026-Q1': { time: '2026-Q1', "传感器校验": 1, "管道清洗": 1, "设备润滑": 5 },
'2026-Q2': { time: '2026-Q2', "传感器校验": 1, "管道清洗": 1, "设备润滑": 2 }
}
通过 Object.values(grouped),我们就得到了 ECharts 需要的数组格式。
第二步:使用 flatMap 动态提取图例
数据准备好了,但图表的图例(legend)和系列(series)不能写死。我们需要从处理好的 data 中动态地提取出所有的任务类型(taskType),也就是每个数据对象中除了 time 以外的所有键。
这时,flatMap 就派上用场了。你可以把它看作是 map + flat 的组合。
-
map: 遍历数组中的每个对象,提取出它的所有键(Object.keys(item))。 -
filter: 过滤掉'time'这个键。 -
flat: 将map产生的多个小数组"拍平"成一个一维数组。 -
Set: 使用Set数据结构去除重复的任务类型。// 假设 data 是 transformQuarterData 处理后的结果
const data = [
{ time: '2026-Q1', "管道清洗": 1, "设备润滑": 5, "传感器校验": 1 },
{ time: '2026-Q2', "管道清洗": 1, "设备润滑": 2, "传感器校验": 1 }
];const taskTypes = [...new Set(data.flatMap(item =>
Object.keys(item).filter(key => key !== 'time')
))];console.log(taskTypes);
// 输出: ['管道清洗', '设备润滑', '传感器校验']
flatMap 执行过程详解:
-
data.map(...)会产生一个二维数组:[ ['管道清洗', '设备润滑', '传感器校验'], // 来自第一个对象 ['管道清洗', '设备润滑', '传感器校验'] // 来自第二个对象 ] -
.flat()会将这个二维数组拍平成一维数组:['管道清洗', '设备润滑', '传感器校验', '管道清洗', '设备润滑', '传感器校验'] -
new Set(...)会去除重复项:Set(3) {'管道清洗', '设备润滑', '传感器校验'} -
最后用扩展运算符
[...Set]将其转回数组。
有了 taskTypes 数组,我们就可以轻松地用它来生成 ECharts 的 legend.data 和 series 配置了。
series: taskTypes.map(type => ({
name: type,
type: 'line',
data: data.map(item => item[type] || 0) // 动态获取每个任务类型的数据
}))
总结
通过这次实战,我们清晰地看到了 reduce 和 flatMap 在数据整形中的强大威力:
reduce:是数据聚合和重组的利器。当你需要将一个复杂的数组转换成一个对象、Map,或者另一个结构完全不同的数组时,reduce是你的首选。它让你在一次遍历中完成所有逻辑,代码既高效又优雅。flatMap:是处理嵌套数组结构的便捷工具。当你需要先对数组元素进行映射(map),而映射的结果又是一个数组,并且你希望最终得到一个扁平化的一维数组时,flatMap比map().flat()更加简洁和语义化。
掌握这两个方法,能让你在处理复杂数据时游刃有余,写出更具现代 JavaScript 风格的代码。