在JavaScript开发中,对象内嵌套数组的数据结构非常常见,尤其是后端接口返回分组数据时。例如一个订单对象中可能包含多个类目分组,每个分组又包含若干商品明细。前端渲染表格或提交数据时,通常需要把这种层级结构转换成扁平的、字段统一的数组。本文会围绕一个典型的分组订单对象展开,说明如何把它转换成适合表格渲染的数组。

理解原始对象与目标结构
先看一个常见的原始数据形态。外层对象保存订单号,groups 数组中的每个元素代表一个商品分组,分组内部又包含 items 数组,用来存放具体的商品明细。这种结构适合后端聚合返回,但前端表格组件往往需要每一行都是一条平铺记录。
const rawData = {
orderNo: 'SO20240501',
groups: [
{
groupId: 1,
name: '数码',
items: [
{ sku: 'P1001', qty: 2, price: 199 },
{ sku: 'P1002', qty: 1, price: 99 }
]
},
{
groupId: 2,
name: '配件',
items: [
{ sku: 'P2001', qty: 3, price: 49 }
]
}
]
};
目标数组需要把外层分组信息与内层明细信息合并到同一个对象中。比如每一条记录都包含分组ID、分组名称、商品编码、数量和单价。这样做之后,每一行不再依赖上一次层级,渲染和筛选都会更方便。
// 目标结构示例
[
{ groupId: 1, groupName: '数码', sku: 'P1001', qty: 2, price: 199 },
{ groupId: 1, groupName: '数码', sku: 'P1002', qty: 1, price: 99 },
{ groupId: 2, groupName: '配件', sku: 'P2001', qty: 3, price: 49 }
]
从原始对象到目标数组,核心工作有两步:第一是遍历外层数组,第二是把每个分组中的内层数组展开,并在展开过程中补充分组信息。接下来分别用不同的数组方法实现这个转换。
使用 map 与 flatMap 完成转换
map 负责对数组中的每个元素执行回调并返回新数组。由于原始结构是两层数组,如果只使用 map,外层分组会被映射成内部数组,最终得到的是一个二维数组。例如对每个分组执行 group.items.map,结果会是 [[...], [...]]。
为了让结果变成一维数组,可以在外层使用 flatMap。flatMap 先执行映射,再自动把返回的数组压平一层。只要回调返回一个数组,flatMap 就会把这些数组元素合并到最终结果中。下面这段代码先遍历分组,再遍历每个分组的 items,并返回一个新对象。
const formattedList = rawData.groups.flatMap(group =>
group.items.map(item => ({
groupId: group.groupId,
groupName: group.name,
sku: item.sku,
qty: item.qty,
price: item.price
}))
);
console.log(formattedList);
使用 flatMap 的优点是代码简洁,意图明确。需要注意它的兼容性:现代浏览器和 Node.js 11 以上版本都支持,但如果项目需要兼容较旧环境,可以改成 map 加 flat 的组合。
const mappedGroups = rawData.groups.map(group =>
group.items.map(item => ({
groupId: group.groupId,
groupName: group.name,
sku: item.sku,
qty: item.qty,
price: item.price
}))
);
const formattedList = mappedGroups.flat();
console.log(formattedList);
这种方式效果相同,只是多了一步。实际项目中如果不需要兼容旧环境,优先使用 flatMap 会让代码更直观。字段重命名也很灵活,只要在返回对象里定义需要的键名即可。
使用 reduce 实现灵活转换与聚合
reduce 比 flatMap 更灵活,它可以在遍历过程中累积任意结果,例如在转换的同时计算金额、统计数量或按条件过滤。回调的第一个参数是累计值,第二个参数是当前元素。我们可以在回调中处理分组,再往累计数组里追加整理好的记录。
const formattedList = rawData.groups.reduce((acc, group) => {
const records = group.items.map(item => {
const total = item.qty * item.price;
return {
groupId: group.groupId,
groupName: group.name,
sku: item.sku,
qty: item.qty,
price: item.price,
total
};
});
acc.push(...records);
return acc;
}, []);
console.log(formattedList);
这里在转换阶段新增了 total 字段,用来表示单项明细的金额。如果后续还需要根据累计值做全局判断,reduce 的优势会更明显。例如只保留价格大于 50 的商品,或者给所有记录添加统一序号,都可以在同一个遍历过程中完成。
不过 reduce 的可读性略低于 flatMap,尤其是回调中直接修改累计数组时,需要记得返回累计值。为了避免隐式修改带来困惑,也可以使用 acc.concat(records) 的方式返回新数组。两者结果一致,选择取决于团队代码风格。
处理空数组、异常数据与去重
真实接口数据往往不够干净,items 可能是 null、undefined,或者分组对象少了某些字段。如果直接在空值上调用 map,会抛出类型错误。因此转换前应该做防御处理。可以用可选链和空值合并运算符来避免报错。
const formattedList = rawData.groups?.flatMap(group => {
const items = Array.isArray(group.items) ? group.items : [];
return items.map(item => ({
groupId: group.groupId ?? 'unknown',
groupName: group.name ?? '未命名',
sku: item.sku ?? '',
qty: item.qty || 0,
price: item.price || 0
}));
}) || [];
console.log(formattedList);
上述代码先使用 rawData.groups?.flatMap 确保 groups 不存在时返回 undefined,最后用 || [] 兜底成空数组。分组内部则用 Array.isArray 判断 items 是否为数组,不是数组时用空数组代替。字段取值时也提供了默认值,避免出现 undefined 污染表格。
另一个常见需求是去重。例如同一分组中可能存在重复的 sku,如果只希望保留第一条,可以借助 Set 配合 filter 实现。也可以直接在转换前对 items 去重,逻辑会更清晰。
const seen = new Set();
const formattedList = rawData.groups.flatMap(group =>
group.items.map(item => ({
groupId: group.groupId,
groupName: group.name,
sku: item.sku,
qty: item.qty,
price: item.price,
key: `${group.groupId}-${item.sku}`
}))
).filter(record => {
if (seen.has(record.key)) {
return false;
}
seen.add(record.key);
return true;
});
console.log(formattedList);
这段代码通过拼接 groupId 和 sku 生成了唯一键,再使用 Set 判断是否已经存在。需要注意的是,去重后的记录中仍然带有 key 字段,如果不想暴露给前端,可以在最后再执行一次 map 删除该字段。
总结来说,对象内嵌套数组转格式化数组并没有固定写法,核心是根据目标结构选择 flatMap 或 reduce,并做好空值保护。只要理解了遍历、映射和扁平化这三步,就能应对大多数实际开发场景。
JavaScript对象嵌套数组数组格式化转换数据扁平化修改时间:2026-08-27 10:28:33