
本文介绍如何基于对象 ID 统计数组中各元素的出现频次,按 ID 降序排列,并生成如 "2x Banana, 1x Apple, 2x Strawberry" 的可读字符串,适用于购物车、库存汇总等场景。
本文介绍如何基于对象 id 统计数组中各元素的出现频次,按 id 降序排列,并生成如 `"2x banana, 1x apple, 2x strawberry"` 的可读字符串,适用于购物车、库存汇总等场景。
要实现这一目标,核心逻辑分为三步:排序 → 聚合计数 → 格式化输出。整个过程应保持函数式风格、避免副作用,并确保 ID 作为唯一标识(同 ID 必然对应相同 name,name 冲突时以首次出现为准)。
✅ 步骤详解
1. 按 ID 降序排序
使用 Array.prototype.toSorted()(ES2023)可安全创建新排序数组,不修改原数据:
data.toSorted((a, b) => b.id - a.id)
若需兼容旧环境(如 Node.js
[...data].sort((a, b) => b.id - a.id) // 推荐:无副作用 // 或 data.slice().sort((a, b) => b.id - a.id)
2. 按 ID 聚合计数(Reduce 构建 Map-like 对象)
利用 reduce() 构建以 id 为键的对象,每个值包含 name(首次出现的 name)和 count:
.reduce((acc, { id, name }) => {
acc[id] ??= { name, count: 0 }; // 空值合并赋值,确保 name 只取第一次
acc[id].count++;
return acc;
}, {})
注意:??= 保证同 ID 的后续项不会覆盖 name,符合“ID 唯一决定商品”的业务前提。
3. 格式化为最终字符串
对聚合结果的 Object.values() 进行映射,首字母大写并拼接:
const capitalise = (str) => str.replace(/\b[a-z]/g, c => c.toUpperCase());
Object.values(counts)
.map(({ name, count }) => `${count}x ${capitalise(name)}`)
.join(", ");
// 输出:"2x Banana, 1x Apple, 2x Strawberry"
? 完整可运行示例
const data = [
{ id: 5, name: "banana" },
{ id: 5, name: "banana" },
{ id: 4, name: "apple" },
{ id: 1, name: "strawberry" },
{ id: 1, name: "strawberry" }
];
const capitalise = (str) => str.replace(/\b[a-z]/g, c => c.toUpperCase());
const counts = data
.toSorted((a, b) => b.id - a.id)
.reduce((acc, { id, name }) => {
acc[id] ??= { name, count: 0 };
acc[id].count++;
return acc;
}, {});
const result = Object.values(counts)
.map(({ name, count }) => `${count}x ${capitalise(name)}`)
.join(", ");
console.log(result); // "2x Banana, 1x Apple, 2x Strawberry"
⚠️ 注意事项
- ID 是唯一性依据:本方案严格依赖 id 判重,不比较 name 或其他字段;若存在相同 ID 不同 name 的异常数据,将保留首个 name。
- 性能友好:时间复杂度 O(n log n)(主要由排序决定),空间复杂度 O(k),k 为唯一 ID 数量。
- 可扩展性:如需支持更多字段(如单价、单位),可在 acc[id] 中扩展属性,格式化阶段同步调整模板字符串。
- 国际化提示:当前 capitalise() 仅处理 ASCII 字母;如需支持 Unicode(如中文、德语变音符号),建议改用 Intl.PluralRules 或专用库处理本地化格式。
该方案简洁、健壮、语义清晰,是处理“按 ID 归类计数+自然语言输出”类需求的标准实践。











