- 开发工具
【免费下载链接】collect.js
💎 Convenient and dependency free wrapper for working with arrays and objects
groupBy() 是 collect.js 提供的高频分组工具,它能把集合中的元素按照某个键或自定义回调的计算结果拆分成“一组一组的集合”,用于数据统计、报表聚合、界面分类展示等场景。本文以 docs/api/groupBy.md 为核心,结合 groupBy 源码 与 groupBy 测试用例 深入讲解其用法、底层实现、边界行为以及与 mapToGroups()、keyBy() 的差异,读完即可在真实项目中熟练完成任意维度(含嵌套字段)的分组。
方法签名与分组语义
groupBy() 接收一个参数key,它既可以是字符串键名,也可以是回调函数:
groupBy<T, K>(key: ((item: Item, index?: number) => K) | keyof Item | K): Collection<T>;(类型签名出自 index.d.ts)
其核心语义是:把集合中的每一项按解析出的分组键归入对应的桶,最终返回一个“以分组键为键、以 Collection 为值”的新集合——也就是说,返回值是一个“集合的集合”。原集合本身不会被修改,这一点与 collect.js 中其他方法保持一致的不可变风格。
按字符串键分组
最常见的用法是直接传入元素对象上的属性名。原文档给出了基于manufacturer(制造商)字段分组的示例:
const collection = collect([ { product: 'Chair', manufacturer: 'IKEA' }, { product: 'Desk', manufacturer: 'IKEA' }, { product: 'Chair', manufacturer: 'Herman Miller' }, ]); const grouped = collection.groupBy('manufacturer'); grouped.all(); // { // IKEA: Collection { items: [...] }, // 包含两条 IKEA 的产品 // 'Herman Miller': Collection { items: [...] } // 包含一条 Herman Miller 的产品 // }分组完成后,grouped的每个值都是一个独立的 Collection 实例,可以继续调用 collect.js 的任何集合方法,例如对每个分组继续做count()、sum()、map()等操作。
按回调函数分组
除了字符串键,还可以传入回调函数。回调会收到当前项item和它的索引index两个参数,返回值就是该元素的分组键。原文档示例按厂商名称的前 3 个字母分组:
const grouped = collection.groupBy((item, key) => item.manufacturer.substring(0, 3)); grouped.all(); // { // IKE: Collection { items: [...] }, // IKEA 的两条产品 // Her: Collection { items: [...] }, // Herman Miller 的一条产品 // }回调的灵活性让它非常适合处理“键名需要经过计算”的场景,例如:按价格区间分组、按时间戳归并到日期、按首字母分组、按字符串长度分组等。
源码实现原理
groupBy的实现非常精简,完整逻辑集中在 src/methods/groupBy.js:
module.exports = function groupBy(key) { const collection = {}; this.items.forEach((item, index) => { let resolvedKey; if (isFunction(key)) { resolvedKey = key(item, index); } else if (nestedValue(item, key) || nestedValue(item, key) === 0) { resolvedKey = nestedValue(item, key); } else { resolvedKey = ''; } if (collection[resolvedKey] === undefined) { collection[resolvedKey] = new this.constructor([]); } collection[resolvedKey].push(item); }); return new this.constructor(collection); };逐行拆解可以看到四个关键设计点:
- 回调优先:通过 src/helpers/is.js 中的
isFunction判断传入的是函数,若是则调用key(item, index),item与index与原生forEach的入参一一对应。 - 字符串键支持嵌套路径:字符串键统一交给 nestedValue 工具函数 解析,它会用
.拆分路径并逐层取值(详见下文)。 - 0 值不落入空桶:注意条件写的是
nestedValue(item, key) || nestedValue(item, key) === 0。这意味着当属性值等于0时,0会被当作合法的分组键保留,而不是被当作 falsy 值归入空字符串桶。这是一个容易踩坑的细节:数字 0、空字符串''、null等 falsy 值中,只有0被显式特殊处理。 - 缺失键兜底为空字符串:当字段不存在或值为 falsy(且不是 0)时,
resolvedKey被设为'',这些元素会统一归入键为''的分组。
分组过程使用普通对象{}累积结果:首次遇到某个键时用new this.constructor([])创建新的 Collection,之后用push()追加元素;最终new this.constructor(collection)把键值对整体包装成新的集合返回。方法注册位于 src/index.js(Collection.prototype.groupBy = require('./methods/groupBy');)。
嵌套键分组(点号路径)
得益于nestedValue,字符串键支持点号语法,可以按对象内部的嵌套字段分组。groupBy 测试用例 中给出了按club.name分组的验证:
const collection = collect([ { name: 'Virgil van Dijk', club: { name: 'Liverpool FC' } }, { name: 'Darwin Núñez', club: { name: 'Liverpool FC' } }, ]); const grouped = collection.groupBy('club.name'); expect(grouped.first()).to.eql(collection); // 两名球员同属 Liverpool FCnestedValue的核心实现只有一行:
key.split('.').reduce((obj, property) => obj[property], mainObject);它把'club.name'拆成['club', 'name'],逐层向下取值;当对象层级不匹配抛出异常时,会通过 try/catch 兜底返回原值(此时说明该元素本身就是值而非对象)。因此groupBy('club.name')等价于对每个元素先做“按 club 再按 name 两级取值”,是处理嵌套数据最优雅的写法。
缺失键的行为:空字符串桶
当集合中部分元素缺少分组键时,它们不会被丢弃,而是统一归入键为''(空字符串)的分组。测试用例用groupBy('price')验证了这一点——products数组中 Herman Miller 的 Chair 没有price字段,最终结果中就会出现一个''分组:
const products = [ { product: 'Catalog', manufacturer: 'IKEA', price: 0 }, { product: 'Desk', manufacturer: 'IKEA', price: 60 }, { product: 'Chair', manufacturer: 'IKEA', price: 60 }, { product: 'Lamp', manufacturer: 'IKEA', price: 15 }, { product: 'Chair', manufacturer: 'Herman Miller' }, // 无 price 字段 ]; const grouped = collect(products).groupBy('price'); grouped.all(); // { // 0: Collection { items: [...] }, // price === 0 被保留为合法键 // 15: Collection { items: [...] }, // 60: Collection { items: [...] }, // '': Collection { items: [...] }, // 缺少 price 的元素归入这里 // }由此可以总结分组键解析的完整规则:
| 传入 key 类型 | 解析逻辑 | 结果 |
|---|---|---|
| 函数 | 调用key(item, index),以返回值作为键 | 键由回调完全决定 |
| 字符串 + 元素含该字段 | 取item[key](支持a.b.c嵌套) | 以字段值为键 |
| 字符串 + 字段值为 0 | 取item[key] | 以0为键(非空字符串) |
| 字符串 + 字段缺失或为 falsy | 兜底 | 以''为键 |
返回“集合的集合”与链式操作
分组结果的特殊性在于:外层是 Collection,每个分组的值也是 Collection。测试用例明确验证了这一点:
const grouped = collection.groupBy('manufacturer'); grouped.first().all(); // 第一个分组(IKEA)的完整元素数组 grouped.last().all(); // 最后一个分组(Herman Miller)的完整元素数组基于这一结构,你可以继续对分组结果做二次加工,例如:
const grouped = collect(products).groupBy('manufacturer'); // 统计每个分组内的元素数量 grouped.map(group => group.count()); // 只取出分组值,丢弃分组键(转成普通数组) grouped.values().all(); // 遍历每个分组 grouped.each((group, manufacturer) => { console.log(manufacturer, group.all()); });与 mapToGroups() 的对比:何时该用谁
原文档专门给出了提示:如果希望得到“普通对象”形式的分组结果,而不是“集合的集合”,请使用 mapToGroups()。
mapToGroups的回调需要返回一个二元数组[key, value],实现位于 src/methods/mapToGroups.js,最终每个键对应的是普通数组:
const collection = collect([ { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' }, { id: 4, name: 'B' }, ]); collection.mapToGroups((item, key) => [item.name, item.id]); // { // A: [1], // B: [2, 4], // C: [3], // }两者的取舍可以这样概括:
- groupBy():按键聚合元素,每个分组是完整的 Collection(元素保持原样),适合后续对分组继续做集合操作;
- mapToGroups():回调显式指定“键 + 映射后的值”,每个分组是普通数组,适合把元素投影成简化结构(如上面的
A: [1])。
与 keyBy() 的对比:合并还是保留全部
与 groupBy 容易混淆的还有 keyBy。二者的本质区别在于同键冲突时的处理:
groupBy():把同键的所有元素保留在同一个分组集合中(不丢数据);keyBy():以键建立映射,同键时后出现的元素覆盖先出现的,最终每个键只保留一个元素(index.d.ts 中的注释明确说明 "If multiple items have the same key, only the last one will appear in the new collection")。
所以在做“按某字段把元素归并成一簇”时用 groupBy,在做“以某字段作为唯一索引查表”时用 keyBy。
测试验证与质量保障
groupBy的行为在 test/methods/groupBy_test.js 中有完整覆盖,共 5 组用例:
- 按给定字符串键分组,且断言原集合
collection.all()保持不变(不可变性); - 支持自定义回调分组(按厂商名前 3 个字符);
- 返回“集合的集合”,
first()/last()可取出对应分组的完整元素; - 缺失键时归入
''分组,同时price: 0的元素被保留在0分组; - 支持
club.name这类嵌套键分组。
这些测试既是行为契约,也是使用时的最佳参考样例。运行方式遵循项目标准流程:
npm install npm test # 内部会先执行 babel transpile,再以 mocha 运行 test/tests.js小结
groupBy()是 collect.js 中最具代表性的“重塑结构”方法之一:字符串键支持点号嵌套路径、回调支持任意计算逻辑、缺失键自动归入空字符串分组、0值被特殊保留,最终产出“集合的集合”以支持深度链式操作。配合 mapToGroups(普通对象结果)与keyBy()(唯一键映射)一起使用,可以覆盖绝大多数数据分组场景;其完整行为定义可在 docs/api/groupBy.md、src/methods/groupBy.js 与 test/methods/groupBy_test.js 中逐一核对。
- 开发工具
【免费下载链接】collect.js
💎 Convenient and dependency free wrapper for working with arrays and objects
相关推荐
Collect.js 终极分组与聚合指南:countBy() 和 groupBy() 的10个实用技巧
Collect.js 终极分组与聚合指南:countBy 和 groupBy 的10个实用技巧 Collect.js 是一个轻量级且无依赖的 JavaScrip
开发工具用 cra-template-particles 快速搭建带 tsParticles 粒子特效的 React 应用:Create React App 官方模板完全指南
用 cra template particles 快速搭建带 tsParticles 粒子特效的 React 应用:Create React App 官方模板完
开发工具collect.js集合运算与合并方法详解
collect.js集合运算与合并方法详解 本文详细介绍了collect.js库中集合运算与合并的核心方法,包括concat、merge、diff、inters
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考