如何用Lodash/Underscore或JS数组函数实现多层数据分组?
问题需求
我们有一组来自TypeORM getRawMany 方法(或Sequelize原生查询)的扁平化对象数组,输入数据如下:
const rawData = [ { "contents_id": "0e362978-ec0a-4851-9f58-e2ad23ec9ee9", "contents_title": "Content mock 1", "contents_created_at": "2020-11-28T23:42:23.717Z", "campaigns_id": "09e34ed1-0dfb-4723-abfb-eec21fc064bc", "campaigns_title": "First campaign title", "product_title": "product title one", "picture_upload": "image_text1.jpg", "views": "1" }, { "contents_id": "0e362978-ec0a-4851-9f58-e2ad23ec9ee9", "contents_title": "Content mock 1", "contents_created_at": "2020-11-28T23:42:23.717Z", "campaigns_id": "09e34ed1-0dfb-4723-abfb-eec21fc064bc", "campaigns_title": "First campaign title", "product_title": "product title two", "picture_upload": "image_text2.jpg", "views": "1" }, { "contents_id": "0e362978-ec0a-4851-9f58-e2ad23ec9ee9", "contents_title": "Content mock 1", "contents_created_at": "2020-11-28T23:42:23.717Z", "campaigns_id": "09e34ed1-0dfb-4723-abfb-eec21fc064bc", "campaigns_title": "First campaign title", "product_title": null, "picture_upload": null, "views": "1" }, { "contents_id": "5b0bc1f1-62ad-4f3e-b2f6-c117a7b46e21", "contents_title": "Content mock 3", "contents_created_at": "2020-11-29T10:11:45.563Z", "campaigns_id": null, "campaigns_title": null, "product_title": null, "picture_upload": null, "views": null }, { "contents_id": "6f211b84-4f76-4da7-9cc7-ab5fe9cc9807", "contents_title": "Content mock 2", "contents_created_at": "2020-11-29T10:07:55.090Z", "campaigns_id": null, "campaigns_title": null, "product_title": null, "picture_upload": null, "views": null }, { "contents_id": "d1658dd6-2dd0-4b8b-b9b2-b3fcd1d2310a", "contents_title": "Content mock 1", "contents_created_at": "2020-11-29T10:07:55.090Z", "campaigns_id": null, "campaigns_title": null, "product_title": null, "picture_upload": null, "views": null } ];
需要将数据按contents进行分组,每个内容条目下嵌套非空的products数组和campaigns数组,最终得到如下格式的结果:
[ { "contents_id": "0e362978-ec0a-4851-9f58-e2ad23ec9ee9", "contents_title": "Content mock 1", "views": 1, "products": [{ "product_title": "product title two", "picture_upload": "image_text2.jpg"}], "campaigns": [{ "campaigns_id": "09e34ed1-0dfb-4723-abfb-eec21fc064bc", "campaigns_title": "First campaign title"}] }, { "contents_id": "6f211b84-4f76-4da7-9cc7-ab5fe9cc9807", "contents_title": "Content mock 2", "products": [], "campaigns": [] }, { "contents_id": "5b0bc1f1-62ad-4f3e-b2f6-c117a7b46e21", "contents_title": "Content mock 3", "products": [], "campaigns": [] } ]
原生JavaScript实现
这是最轻量化的方案,利用数组reduce方法完成分组,同时处理嵌套数组的空值过滤与去重,无需依赖任何第三方库:
const result = Object.values(rawData.reduce((acc, item) => { // 以contents_id作为分组唯一标识 const key = item.contents_id; // 初始化当前内容的基础结构 if (!acc[key]) { acc[key] = { contents_id: item.contents_id, contents_title: item.contents_title, views: item.views ? Number(item.views) : undefined, products: [], campaigns: [] }; } // 处理产品:仅保留非空且未重复的条目 if (item.product_title) { const isDuplicate = acc[key].products.some(p => p.product_title === item.product_title); if (!isDuplicate) { acc[key].products.push({ product_title: item.product_title, picture_upload: item.picture_upload }); } } // 处理活动:仅保留非空且未重复的条目 if (item.campaigns_id) { const isDuplicate = acc[key].campaigns.some(c => c.campaigns_id === item.campaigns_id); if (!isDuplicate) { acc[key].campaigns.push({ campaigns_id: item.campaigns_id, campaigns_title: item.campaigns_title }); } } return acc; }, {}));
Lodash实现
如果项目已经引入Lodash,可以借助它的链式调用能力让代码更简洁易读:
const _ = require('lodash'); const result = _.chain(rawData) // 按contents_id分组 .groupBy('contents_id') .map(group => { // 提取内容基础信息 const contentBase = _.pick(group[0], ['contents_id', 'contents_title']); contentBase.views = group[0].views ? Number(group[0].views) : undefined; // 生成去重后的非空产品数组 contentBase.products = _.chain(group) .filter(item => item.product_title) .map(item => _.pick(item, ['product_title', 'picture_upload'])) .uniqBy('product_title') .value(); // 生成去重后的非空活动数组 contentBase.campaigns = _.chain(group) .filter(item => item.campaigns_id) .map(item => _.pick(item, ['campaigns_id', 'campaigns_title'])) .uniqBy('campaigns_id') .value(); return contentBase; }) .value();
实现说明
- 两种方案都完成了空值过滤:仅保留有实际内容的产品和活动
- 都处理了重复数据:同一个内容下的相同产品/活动只保留一次
- 将字符串类型的
views转换为数字类型,与期望输出格式完全匹配
内容的提问来源于stack exchange,提问作者Julien Ambrosio
相关产品推荐
相关产品推荐

