- 开发工具
【免费下载链接】collect.js
💎 Convenient and dependency free wrapper for working with arrays and objects
countBy()是 collect.js 提供的频次统计方法,用于统计集合中各元素出现的次数,既可默认按元素本身计数,也可传入回调函数按任意自定义维度(如邮箱域名、首字母、日期、状态值)聚合计数。读完本文,你将掌握countBy()的两种调用形态、返回值的真实结构与取数方式,并通过源码级拆解理解其"分组 + 计数"的底层实现原理,可直接用于日志统计、数据透视、投票计数等真实业务场景。
collect.js 是一个零依赖、API 与 Laravel Collection 高度一致的数组与对象操作封装库(见 README.md 与 package.json),countBy()是其中常用的聚合方法之一。
方法签名与两种调用形态
countBy()的方法定义位于 src/methods/countBy.js,本质是一个只有一个可选参数的函数:
'use strict'; module.exports = function countBy(fn = value => value) { return new this.constructor(this.items) .groupBy(fn) .map(value => value.count()); };从签名可以看出:
- 参数
fn(可选):一个回调函数,用于从每个元素中提取"分组键"(即统计维度)。不传时默认值为value => value,即恒等函数,直接以元素本身作为键进行计数。 - 返回值:
countBy()返回一个新的 Collection 实例,不会修改原集合,其底层数据是一个以分组键为属性名、以出现次数为属性值的对象(需要通过.all()取出)。
也就是说,countBy()在源码层只做了三件事:groupBy(fn)按回调结果分组 →map(...)把每个分组映射为其成员数量 → 返回新的集合。下面分别看两种调用形态的具体表现。
默认计数:统计每个元素出现的次数
不传任何参数时,countBy()会按元素本身的值进行分组并计数。这是文档给出的第一个示例:
const collection = collect([1, 2, 2, 2, 3]); const counted = collection.countBy(); counted.all(); // { // 1: 1, // 2: 3, // 3: 1, // }即:数字1出现 1 次,2出现 3 次,3出现 1 次。这种用法对字符串、布尔值、数字等原始类型都适用,测试用例 test/methods/countBy_test.js 分别验证了三种情形:
// 字符串 collect(['foo', 'foo', 'foo', 'bar', 'bar', 'foobar']).countBy().all(); // { foo: 3, bar: 2, foobar: 1 } // 布尔值 collect([true, true, false, false, false]).countBy().all(); // { true: 2, false: 3 } // 整数 collect([1, 5, 1, 5, 5, 1]).countBy().all(); // { 1: 3, 5: 3 }需要注意,返回结果的键是 JavaScript 对象的属性名(会被隐式转为字符串),但通过to.eql断言比较时数值键与字符串键是等价的,测试中{ 1: 3, 5: 3 }与预期完全一致。
回调计数:按自定义维度聚合统计
当需要按某个派生维度(而不是元素本身)统计时,传入一个回调函数即可。回调接收当前元素(在分组实现中还接收索引,详见下文源码分析),返回用于分组的键值。文档给出的邮箱域名统计是经典场景:
const collection = collect([ 'mohamed.salah@gmail.com', 'darwin.nunez@yahoo.com', 'roberto.firmino@gmail.com', ]); const counted = collection.countBy(email => email.split('@')[1]); counted.all(); // { // 'gmail.com': 2, // 'yahoo.com': 1, // }这里回调email => email.split('@')[1]提取出每个邮箱的域名部分,countBy()按域名分组后得到gmail.com出现 2 次、yahoo.com出现 1 次。
测试 test/methods/countBy_test.js 还演示了按字符串首字母分组:
const collection = collect(['alice', 'aaron', 'bob', 'carla']); collection.countBy(value => value[0]).all(); // { a: 2, b: 1, c: 1 }以及再次验证的邮箱域名场景。回调的返回值没有类型限制,可以是字符串、数字、布尔值甚至对象(对象会作为键名被转成字符串),这为各种业务维度的频次统计提供了极大的灵活性。
返回值结构:新的集合实例与取数方式
countBy()返回的是一个新的 Collection 实例(而非普通对象),原因在于源码中使用了new this.constructor(this.items)来重新构造集合。这意味着:
- 原集合不受影响:与 collect.js 多数方法(如 map())一样,
countBy()不会修改调用它的集合,而是产出新实例。 - 需要
.all()取出底层数据:如文档示例所示,通过counted.all()才能拿到形如{ 'gmail.com': 2, 'yahoo.com': 1 }的普通对象,进一步用于JSON.stringify、渲染或后续处理。all()的实现见 src/methods/all.js,它返回集合底层承载的数组或对象。 - 可继续链式调用:既然返回值是 Collection 实例,就可以继续串联其他方法,例如
collection.countBy(email => email.split('@')[1]).sortByDesc()按域名频次降序排序,或.pipe()后接自定义处理(参见 pipe() 文档)。
对于空集合,collect([]).countBy().all()返回{};对于只含单一重复值的集合,也会得到只有一项的对象,行为符合直觉。
源码级实现原理:groupBy + map(count) 的两段式流水线
countBy()的三行实现是整个方法的精髓,理解它就能触类旁通。它复用了 collect.js 的groupBy、map、count三个方法,形成一条"分组 → 计数"流水线。
第一段:groupBy(fn)按回调结果分组
分组逻辑位于 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); // 传入回调时:回调(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); };关键点有二:
- 回调签名:分组回调实际收到
(item, index)两个参数(isFunction判断来自 src/helpers/is.js),因此你在countBy((value, key) => ...)中还可以拿到元素索引; - 分组结果:每个分组值都是一个 Collection 子实例,这正是下一步
value.count()能直接调用count()的前提。
第二段:map(value => value.count())将每个分组替换为成员数
map()的实现见 src/methods/map.js:当底层是数组时使用原生Array.prototype.map,当底层是对象时遍历键并把每个值交给回调。在这里,map遍历由groupBy产生的分组对象,对每个分组的子集合调用count(),得到该组元素数量。
count()的实现见 src/methods/count.js,它对数组直接取length,对对象取Object.keys().length,两者取较大值:
module.exports = function count() { let arrayLength = 0; if (Array.isArray(this.items)) { arrayLength = this.items.length; } return Math.max(Object.keys(this.items).length, arrayLength); };因此map(value => value.count())执行完毕后,原集合[1, 2, 2, 2, 3]被转换为分组对象{ 1: [1], 2: [2, 2, 2], 3: [3] },再映射为{ 1: 1, 2: 3, 3: 1 }——与文档展示的输出完全吻合。整个countBy方法的注册入口在 src/index.js,可通过Collection.prototype.countBy直接调用。
与 Laravel 语义的一致性
collect.js 的 API 设计对齐 Laravel Collection(见 docs/README.md 的特性说明),countBy的语义与 Laravel 同名方法一致:默认按元素值统计,传回调则按回调返回值统计。熟悉 Laravel 的开发者可以无缝迁移到前端或 Node.js 环境。
典型应用场景
结合上述能力,countBy()可应用于以下常见业务:
- 邮件/用户数据透视:按邮箱域名统计用户来源(即文档示例),或按
user.group_id统计各分组人数; - 日志分析:按日志级别统计错误、警告、信息各出现多少条;
- 投票与问卷统计:统计每个选项被选中的次数,或按选项前缀聚合;
- 数据质量检查:统计数组中重复值的分布,配合 count() 快速判断数据去重前后的规模差异;
- 前端状态聚合:统计 UI 列表中各状态(进行中/已完成/已失败)的数量,直接驱动徽标或图表展示。
相关方法延伸
理解countBy()后,可以继续探索其协作方法与变体:
- count():返回集合中元素总数,与
countBy()的"分组内计数"互补; - groupBy():
countBy()的第一段实现,支持字符串键与回调两种分组方式,可直接查看分组结果; - map():
countBy()的第二段实现,用于逐项变换; - mapToGroups():如果需要以普通对象形式分组(而非嵌套集合),可参考该方法的思路;
- pipe():在
countBy()之后串联自定义聚合逻辑,实现更复杂的统计流水线。
若需在 TypeScript 项目中调用,可直接沿用 collect.js 的通用引入方式import collect from 'collect.js';(用法示例见 docs/usage.md),再对结果调用.all()获取普通对象。运行本项目测试(npm test,测试入口 test/tests.js)可进一步验证countBy的全部行为。
- 开发工具
【免费下载链接】collect.js
💎 Convenient and dependency free wrapper for working with arrays and objects
相关推荐
Gradle 空值安全实战指南:基于 JSpecify 与 NullAway 的非空默认注解体系
Gradle 空值安全实战指南:基于 JSpecify 与 NullAway 的非空默认注解体系 本篇指南系统讲解 Gradle Build Tool 代码库中
构建工具开发工具RailsAdmin 字段默认值设置指南:模型回调、URL 参数与 current_user 实战
RailsAdmin 字段默认值设置指南:模型回调、URL 参数与 current_user 实战 RailsAdmin 在新建记录的表单中默认不预填任何值(下
后端Ice 免费菜单栏 3 区图标管理,10 分钟装好调顺
Ice 免费菜单栏 3 区图标管理,10 分钟装好调顺 Mac 菜单栏被应用图标挤满,Wi Fi 和电池被顶到屏幕边缘。Ice 是一款免费开源的 Mac 菜单栏
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考