JavaScript 新特性实战:用 Object.groupBy 告别 reduce 实现数组分组

在业务代码里给数组分组是再常见不过的操作了。有一组订单,想按状态分成“待支付”“已发货”“已完成”;有一组用户,想按年龄段归类;有一组日志,想按级别汇总。每次遇到这种需求,很多人的第一反应是拿出 reduce,写一个累加器,往对象里塞键值对。一个两个还好,但项目里到处都是相似的 reduce 模板,阅读起来得先反应一下这个回调到底在干什么,维护久了容易审美疲劳。

好在 ES2024 正式带来了 Object.groupByMap.groupBy 两个静态方法,专门用来做数组分组。用法跟 reduce 完全不同:你只需要提供一个分组键的回调函数,返回的结果自动按组归类。这篇文章以一个订单报表的案例为主线,先把传统 reduce 分组的写法亮出来,再用 groupBy 重写,最后补充一些迁移注意点和边界情况。

传统 reduce 分组:能跑但费眼

假设订单列表长这样:

const orders = [
    { id: '001', status: 'pending', amount: 99 },
    { id: '002', status: 'shipped', amount: 150 },
    { id: '003', status: 'pending', amount: 60 },
    { id: '004', status: 'completed', amount: 200 },
    { id: '005', status: 'shipped', amount: 80 },
    { id: '006', status: 'pending', amount: 120 },
];

按照 status 分组,传统做法是这样:

const groupedByStatus = orders.reduce((acc, order) => {
    const key = order.status;
    if (!acc[key]) {
        acc[key] = [];
    }
    acc[key].push(order);
    return acc;
}, {});

console.log(groupedByStatus);
// 输出:
// {
//   pending: [ { id:'001', ... }, { id:'003', ... }, { id:'006', ... } ],
//   shipped: [ { id:'002', ... }, { id:'005', ... } ],
//   completed: [ { id:'004', ... } ]
// }

这段代码能正常工作,但有几个让人不舒服的点。第一,acc 的初始值是一个空对象,分组键动态生成,类型检查工具很难推断返回值中各键的含义。第二,if (!acc[key]) 这一行在实际写的时候经常被忘记,导致第一次碰到新键时因为 undefined 而报错。第三,reduce 本身表达的意图是“累加”,用它来做分组其实是把分组逻辑强塞进累加器的壳里,不够自解释。

如果分组键需要某种转换(比如按年份归类),reduce 回调还会膨胀:

const ordersByYear = orders.reduce((acc, order) => {
    const year = new Date(order.createdAt).getFullYear();
    if (!acc[year]) {
        acc[year] = [];
    }
    acc[year].push(order);
    return acc;
}, {});

多写几个这样的分组函数之后,团队里就会出现各种封装工具函数试图简化,但最终又回到了自己造的轮子上。

Object.groupBy:一行代码完成分组

Object.groupBy(array, callback) 接受两个参数:要分组的数组和一个用来提取分组键的函数。返回值是一个原型为 null 的对象,这意味着它不会继承 Object.prototype 的任何方法(如 toStringhasOwnProperty),纯粹是一个键值映射。这其实是个优点——防止键名冲突(比如键恰好叫 toString)。

Object.groupBy 重写上面的分组,代码量骤减:

const groupedByStatus = Object.groupBy(orders, (order) => order.status);
console.log(groupedByStatus);
// 结果与 reduce 版本相同,但对象原型为 null

不需要手动初始化数组,不需要 if 判断键是否存在,阅读的人一眼就看出“这是按 status 分组”。如果分组键需要转换,直接在第二个回调里处理:

const ordersByYear = Object.groupBy(orders, (order) => 
    new Date(order.createdAt).getFullYear()
);

Object.groupBy 返回的对象原型为 null,意味着你不能用 .hasOwnPropertytoString,但可以直接用 Object.keysObject.entriesin 操作符来遍历。这对分组场景来说几乎没有任何负面影响。

Map.groupBy:当分组键不是字符串时

Object.groupBy 的所有键都会被强制转换成字符串或 Symbol。如果分组键本身是对象、数字或你希望保留原始类型,需要用到 Map.groupBy。它返回一个标准的 Map 实例,键可以是任意类型。

比如,订单对象里有一个 customer 字段,它是个对象引用,我们想按客户对象来分组(同一个客户对象的订单归到一组):

const customerA = { id: 'c1', name: '张三' };
const customerB = { id: 'c2', name: '李四' };

const ordersWithCustomer = [
    { id: '1', amount: 50, customer: customerA },
    { id: '2', amount: 80, customer: customerB },
    { id: '3', amount: 30, customer: customerA },
];

const byCustomer = Map.groupBy(ordersWithCustomer, (o) => o.customer);

console.log(byCustomer.get(customerA));
// [{ id:'1', ... }, { id:'3', ... }]
console.log(byCustomer.get(customerB));
// [{ id:'2', ... }]

Map.groupBy 在需要对分组结果做进一步操作时也更顺手,因为 Map 自带了 forEachentrieshas 等遍历和查询方法,不需要先转成数组再操作。

完整案例:订单报表生成

现在来一个更贴近实际需求的例子。系统需要生成一份按订单状态汇总的报表,每个状态下包含订单列表以及该状态的总金额。用 Object.groupBy 搭配 Array.mapreduce 写一个干净的数据处理管线:

const orders = [
    { id: '1', status: 'pending', amount: 99 },
    { id: '2', status: 'shipped', amount: 150 },
    { id: '3', status: 'pending', amount: 60 },
    { id: '4', status: 'completed', amount: 200 },
    { id: '5', status: 'shipped', amount: 80 },
];

// 第一步:按状态分组
const byStatus = Object.groupBy(orders, (o) => o.status);

// 第二步:为每个状态生成汇总
const report = Object.entries(byStatus).map(([status, items]) => {
    const totalAmount = items.reduce((sum, item) => sum + item.amount, 0);
    return {
        status,
        count: items.length,
        totalAmount,
        orders: items,
    };
});

console.log(report);
// [
//   { status: 'pending', count: 2, totalAmount: 159, orders: [...] },
//   { status: 'shipped', count: 2, totalAmount: 230, orders: [...] },
//   { status: 'completed', count: 1, totalAmount: 200, orders: [...] },
// ]

整个流程没有出现任何显式的键检查,也没有去操作一个中间累加器。先从 Object.groupBy 拿到分组对象,再用 Object.entries 展开成数组做二次计算。读起来几乎就是把自然语言的步骤翻译成了代码。

如果你更倾向于直接拿到一个 Map,以便后续再按其他维度做操作,可以换成 Map.groupBy,再把 Map 的 forEach 当作遍历手段。

与 reduce 的性能差异?

理论上 Object.groupBy 的内部实现也是遍历一次并收集结果,和手写 reduce 的效率在同一量级。因为在 V8 中是原生实现,省略了 JavaScript 层的回调调用和动态键检查,实际性能往往比手写 reduce 稍快。但在绝大多数业务场景里,这点性能差异远小于代码可读性带来的收益。

如果数组有几十万项,分组操作本身的复杂度是 O(n),原生实现可能会有优化,但瓶颈通常不在这里,而在于后续的数据处理或 UI 渲染。所以放心用,不必担心性能退化。

浏览器兼容与迁移策略

Object.groupByMap.groupBy 已经在 2024 年下半年被所有主流浏览器原生支持,包括 Chrome 117+、Edge 117+、Firefox 119+、Safari 17.4+。Node.js 从 21.0 开始也支持。如果你的运行环境是较新的浏览器或 LTS 版本的 Node,可以直接用而不需要 polyfill。

对于暂时需要支持旧环境的项目,可以引入 core-js 的 polyfill(core-js/proposals/array-grouping),也可以在构建阶段用 Babel 转译。不过这两个方法是静态方法,不是数组原型上的方法,polyfill 相当轻量。如果实在不好加 polyfill,继续用 reduce 也不会错,只是代码会多一些。

迁移的时候,没有必要把项目里所有的 reduce 分组一次性替换掉。建议在新增功能或重构某模块时顺手改掉,让改进随着正常开发节奏逐步渗透。

其他你可能不知道的细节

回调可以返回 undefined。 如果分组回调返回 undefined,这个元素会被丢弃,不会出现在分组结果中。这可以用来过滤某些不需要的值。比如,只想保留有 owner 属性的任务:

const tasks = [
    { name: 'task A', owner: 'Alice' },
    { name: 'task B' },  // 没有 owner
    { name: 'task C', owner: 'Bob' },
];

const byOwner = Object.groupBy(tasks, (t) => t.owner);
// 结果: { Alice: [...task A...], Bob: [...task C...] }
// 没有 owner 的 task B 被丢弃了

这实际上是把“过滤”和“分组”合并在了一步,但语义是隐式的。如果团队中有人不熟悉这个行为,可能会意外丢掉数据,建议在回调中显式处理返回 undefined 的场景,或者直接改用 filter + groupBy 的组合。

空数组直接返回空对象/空 Map。 不会报错:

Object.groupBy([], (x) => x.type); // 返回原型为 null 的空对象 {}
Map.groupBy([], (x) => x.type);     // 返回空 Map(0)

这比 reduce 省了一个初始值的检查,直接就能安全使用。

总结

Object.groupByMap.groupBy 不是什么颠覆性的语言特性,但它们把最常见的数据转换操作之一——数组分组——从每次都要亲手写出 if-acc 的逻辑中解放了出来。代码更短,意图更清晰,减少了忘记初始化键的低级错误。订单报表的例子只是一个起点,几乎任何需要按维度聚合的业务报表、数据看板、分析模块都能用这套方法简化。

如果你还在用 reduce 写分组,下次写新功能时可以试试 groupBy。一用就回不去,很可能你回头就去重构老代码。

JavaScript 新特性实战:用 Object.groupBy 告别 reduce 实现数组分组
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript JavaScript 新特性实战:用 Object.groupBy 告别 reduce 实现数组分组 https://www.taomawang.com/web/javascript/2464.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务