groupBy:别再手写 reduce 分组了
ES2024 的 Object.groupBy / Map.groupBy 用回调直接分组,省掉 reduce + ??= [] 样板;字符串键用 Object,对象键用 Map(Node 21+ / 2024-03 起浏览器广泛可用)。
要解决的问题
前端里「按字段分组」太常见了:订单按状态拆成待付款 / 已发货,标签页按 type 分栏,库存按库存量分成「够用」和「要补货」。很多人第一反应是手写 reduce:
const byType = items.reduce((acc, item) => {
(acc[item.type] ??= []).push(item);
return acc;
}, {});
能跑,但每次都要记得初始化空数组、别写错累加器、还得想清楚键是字符串还是别的东西。ES2024 起,语言自带了静态方法 Object.groupBy 和 Map.groupBy:传入可迭代对象和一个回调,直接得到分组结果。MDN 标为 Baseline Widely available(各主流浏览器约自 2024-03 起具备);Node.js 需 21+(本机示例在 Node.js v22.21.0 跑通;系统自带的 Node 20 尚无此 API)。
核心概念
两个 API 的签名几乎一样,差别在「键」和「返回值」:
Object.groupBy(items, callbackFn) // → 无原型对象(null-prototype)
Map.groupBy(items, callbackFn) // → Map
items:任意可迭代对象(数组最常见)。callbackFn(element, index):对每个元素返回分组键。Object.groupBy:键会被当成属性键(字符串或 symbol;其它类型会强制转成字符串)。返回的是Object.create(null)那种没有原型的对象,每个键对应一个数组。Map.groupBy:键可以是任意值(对象、数字、Date……),按 SameValueZero / 引用身份 进Map。适合「用对象当分组标签」的场景。
分组结果里的元素和原数组是同一份引用,不是深拷贝。改元素内部字段,两边都能看到——这点和 reduce 推进去的是同一回事。
早期提案里曾出现过 Array.prototype.group / groupToMap,后来因 Web 兼容性改成了现在的静态方法。别再搜 arr.group(...),标准形态就是 Object.groupBy / Map.groupBy。
选用口诀很简单:键能用字符串/symbol 表达 → Object.groupBy;键必须是对象或要保留非字符串身份 → Map.groupBy。
示例代码
下面片段已在本机 Node 22 跑过断言。
按 type 分组(最常见):
const inventory = [
{ name: "asparagus", type: "vegetables", quantity: 9 },
{ name: "bananas", type: "fruit", quantity: 5 },
{ name: "goat", type: "meat", quantity: 23 },
{ name: "cherries", type: "fruit", quantity: 12 },
{ name: "fish", type: "meat", quantity: 22 },
];
const byType = Object.groupBy(inventory, ({ type }) => type);
console.log(byType.fruit);
// [
// { name: "bananas", type: "fruit", quantity: 5 },
// { name: "cherries", type: "fruit", quantity: 12 }
// ]
console.log(Object.getPrototypeOf(byType)); // null
按业务规则分组(库存是否够用):
const byStock = Object.groupBy(inventory, ({ quantity }) =>
quantity < 6 ? "restock" : "sufficient"
);
console.log(byStock.restock);
// [{ name: "bananas", type: "fruit", quantity: 5 }]
数字等非字符串键会被强制转成字符串属性名:
const nums = [1, 2, 3, 4, 5];
const byParity = Object.groupBy(nums, (n) => n % 2);
console.log(byParity["0"]); // [2, 4]
console.log(byParity["1"]); // [1, 3, 5]
需要对象键时用 Map.groupBy(注意必须用同一个对象引用去 get):
const restock = { label: "restock" };
const sufficient = { label: "sufficient" };
const grouped = Map.groupBy(inventory, ({ quantity }) =>
quantity < 6 ? restock : sufficient
);
console.log(grouped.get(restock));
// [{ name: "bananas", type: "fruit", quantity: 5 }]
const lookalike = { label: "restock" };
console.log(grouped.get(lookalike)); // undefined:长得像没用,必须是原引用
对照一眼旧写法:Object.groupBy 省掉了累加器样板,意图也更直白——「按这个回调分组」,而不是「我在 reduce 里拼对象」。
常见误区
-
拿返回对象调
hasOwnProperty/toString
Object.groupBy的结果是 null 原型对象,没有继承自Object.prototype的方法。groups.hasOwnProperty("fruit")会直接抛错。查键用Object.hasOwn(groups, "fruit"),或'fruit' in groups。 -
以为键「原样保留」
Object.groupBy会把非字符串/非 symbol 的返回值转成字符串。n % 2得到的是数字0/1,属性名却是"0"/"1"。需要保留数字或对象身份时,改用Map.groupBy。 -
Map.groupBy用「长得一样的新对象」去取
Map 的对象键按引用比。分组时用的那个{ restock: true },取值时还必须是同一个变量;重新const k = { restock: true }再get(k)会得到undefined。 -
以为分组结果是深拷贝
数组里的元素还是原对象。改byType.fruit[0].quantity,原inventory里对应项也会变。若要独立快照,分组后再自己拷贝。 -
还在写
arr.group(...)
提案早期的实例方法没有成为标准。正确写法是静态的Object.groupBy(arr, fn)/Map.groupBy(arr, fn)。 -
运行环境过旧
浏览器侧约 2024-03 起广泛可用(Chrome 117+、Firefox 119+、Safari 17.4+ 量级);Node 需 21+。还在 Node 20 LTS 上跑构建脚本的话,要么升到 22 LTS,要么继续用reduce/ 工具库,不要假设全局已有Object.groupBy。 -
空分组不会出现
回调从未返回过的键,结果里就没有对应属性/Map条目。UI 若要固定展示「所有状态栏」(哪怕某栏是空数组),需要自己补默认键,不能指望groupBy预填。
小结
按字段或规则把列表拆成多组时,优先用 Object.groupBy;键是对象或必须保留非字符串身份时,用 Map.groupBy。它们替代了大半「reduce + ??= []」样板,返回值分别是 null 原型对象和 Map。记住:元素是浅引用、对象键看身份、null 原型上别调 hasOwnProperty,环境要 Node 21+ / 近一年的 evergreen 浏览器。细节以 MDN:Object.groupBy 与 Map.groupBy 为准。
参考资料
谢谢你读到这里。