☰
collect.js countBy() 方法完全指南:基于默认值与回调的频次统计实战
2026/10/15 16:35:04 网站建设 项目流程
  • 开发工具

【免费下载链接】collect.js

💎 Convenient and dependency free wrapper for working with arrays and objects

项目地址:https://gitcode.com/gh_mirrors/co/collect.js
点击查看免费下载

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)来重新构造集合。这意味着:

  1. 原集合不受影响:与 collect.js 多数方法(如 map())一样,countBy()不会修改调用它的集合,而是产出新实例。
  2. 需要.all()取出底层数据:如文档示例所示,通过counted.all()才能拿到形如{ 'gmail.com': 2, 'yahoo.com': 1 }的普通对象,进一步用于JSON.stringify、渲染或后续处理。all()的实现见 src/methods/all.js,它返回集合底层承载的数组或对象。
  3. 可继续链式调用:既然返回值是 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

项目地址:https://gitcode.com/gh_mirrors/co/collect.js
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询