ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

collect.js 的 average() / avg() 方法:三种平均值计算方式与源码级原理解析

collect.js 的 average() / avg() 方法:三种平均值计算方式与源码级原理解析
  • 开发工具

【免费下载链接】collect.js

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

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

average()是 collect.js 集合库中用于计算集合内所有元素平均值的便捷方法,它与avg()完全等价(avg()是实际实现、average()是别名)。本文围绕 average() 官方文档 与配套的 avg() 文档,结合 average.js 源码、底层 sum.js 与 pluck.js 实现,以及 average_test.js、avg_test.js 测试用例,完整讲解无参求和、按键取值、回调函数三种调用形态,并深入其背后的求和与取值原理、边界行为与 TypeScript 类型签名,读完即可在真实项目中正确、稳健地使用该 API。

方法定位:average() 是 avg() 的别名

average()文档本身只有一句话定义:它是avg()方法的别名(Alias for theavg()method)。这一点在源码中有三重印证:

  1. 实现文件直接复用:src/methods/avg.js 全文仅一条语句,直接require('./average')导出,说明avg与average指向同一个函数对象:
    'use strict'; const average = require('./average'); module.exports = average;
  2. 注册入口同时挂载:src/index.js 中Collection.prototype.average与Collection.prototype.avg分别加载两个方法文件,因此无论调用哪一个,行为完全一致。
  3. 测试用例明确验证别名关系:avg_test.js 断言collect([1, 3, 3, 7]).avg()与collect([1, 3, 3, 7]).average()结果相同。

所以下面关于avg()的每一种用法与行为,对average()同样成立,文章中不再重复区分。

基本用法:无参数计算所有元素的平均值

当集合中的元素本身就是数值时,直接调用avg()即可,无需任何参数:

collect([1, 3, 3, 7]).avg(); // 3.5

对应的average()写法:

collect([1, 3, 3, 7]).average(); // 3.5

从 average.js 源码 看,无参分支的实现是:

module.exports = function average(key) { if (key === undefined) { return this.sum() / this.items.length; } // ... };

即:先调用集合自身的sum()求出元素总和,再除以元素个数this.items.length,得到算术平均值。这里的sum()实现位于 src/methods/sum.js,它对每个元素执行parseFloat(items[i])并累加,因此数字字符串(如'3')也会被解析为数值参与运算。

按键(key)计算嵌套对象属性的平均值

当集合元素是对象或嵌套数组时,需要传入一个key,指明用哪个属性的值参与求平均:

const collection = collect([ { name: 'My story', pages: 176, }, { name: 'Fantastic Beasts and Where to Find Them', pages: 1096, }, ]); collection.avg('pages'); // 636

对应average('pages')结果相同,即(176 + 1096) / 2 = 636。

该分支的源码实现为 average.js:

return new this.constructor(this.items).pluck(key).sum() / this.items.length;

它通过new this.constructor(this.items)基于原集合元素重建一个新集合(不会修改原集合,这一点由 average_test.js 断言collection.all()仍等于原products得到验证),然后调用pluck(key)抽出对应属性的值,再求和、除以原集合长度。

得益于 pluck.js 内部使用 nestedValue.js 解析属性路径(后者支持用.分隔的多级路径,例如key.split('.').reduce(...)),avg()的key参数同样支持嵌套路径写法,例如:

collect([ { order: { amount: 100 } }, { order: { amount: 300 } }, ]).avg('order.amount'); // 200

使用回调函数自定义取值逻辑

当平均值的计算逻辑不能用一个简单属性名表达时(例如需要对属性做变换、过滤后取值,或取值来自多个属性组合),可以传入一个回调函数,collect.js 会以每个元素为参数调用它:

const collection = collect([ { name: 'My story', pages: 176, }, { name: 'Fantastic Beasts and Where to Find Them', pages: 1096, }, ]); collection.avg(book => book.pages); // 636

回调分支的源码为 average.js:

if (isFunction(key)) { return new this.constructor(this.items).sum(key) / this.items.length; }

其中isFunction来自 src/helpers/is.js,本质是typeof item === 'function'。传入的sum(key)会对每个元素执行parseFloat(key(items[i]))后累加,所以回调的返回值既可以是数值,也可以是可被parseFloat解析的字符串。

回调形态是三种调用方式中扩展性最强的:你可以在回调里做item.pages * 2、拼接多个字段、甚至先过滤再映射,因为sum()只会消费回调的返回值。

源码级原理:average() 的三分支结构与底层依赖

把 average.js 完整展开,其逻辑清晰分为三条路径:

调用形态条件求值方式
无参key === undefinedthis.sum() / this.items.length
回调函数isFunction(key)new this.constructor(this.items).sum(key) / this.items.length
属性名其他情况new this.constructor(this.items).pluck(key).sum() / this.items.length

三个分支共享两个关键设计:

  1. 复用而非重写:average不自己遍历元素,而是委托给集合自身的sum()与pluck()方法。这种组合式实现让平均值计算天然继承了sum()的精度处理和pluck()的嵌套取值能力,代码量极小且行为统一。
  2. 不变性(immutability):带参分支通过new this.constructor(this.items)基于原元素重建集合再求值,原集合数据不会被改写;无参分支直接读取this.items也仅做读取。

关于浮点精度,值得留意sum()的收尾处理——sum.js 返回parseFloat(total.toPrecision(12)),即把累加结果先按 12 位有效数字四舍五入再转回数字。这一处理能削减大部分累加误差,但除法引入的小数位仍可能超出预期,例如测试用例中的collect([1, 3, 3, 7]).unique().avg()得到3.6666666666666665(而非精确的3.666...6),这是 IEEE 754 浮点数的正常表现,需要精确结果时应自行toFixed处理。

测试验证:三种形态均有对应用例

仓库测试 test/methods/average_test.js 覆盖了average()的全部三种调用形态,可直接作为行为规范参考:

it('should return the average value of collection values', () => { expect(collect([1, 3, 3, 7]).average()).to.eql(3.5); }); it('should accept a callback function', () => { const avg = collect(products).average(product => product.price); expect(avg).to.eql(550); }); it('should return the average value of collection values by key', () => { const collection = collect(products); const avg = collection.average('price'); expect(avg).to.eql(550); expect(collection.all()).to.eql(products); });

测试数据products = [{ name: 'Chair', price: 600 }, { name: 'Desk', price: 900 }, { name: 'Lamp', price: 150 }]的均价为(600 + 900 + 150) / 3 = 550。

而 avg_test.js 除同样覆盖按键、回调外,还专门验证了别名等价性与链式组合(unique().avg()),证明avg/average可以与unique()等方法流畅串联。

边界情况与注意事项

结合源码可以确认以下几点行为,供实战时规避坑点:

  • 空集合:无参分支中sum()对空集合返回0(sum.js 循环不执行,total保持 0),再除以this.items.length(0),最终得到NaN。从 average.js 的实现结构可以推断,对空集合调用average()/avg()不会抛错,但会返回NaN,业务上应先判空。
  • 混合类型元素:sum()对所有元素统一执行parseFloat,非数字内容会被解析为NaN并污染累加结果,最终均价同样可能变成NaN,因此参与求平均的元素应保证为数值或数字字符串。
  • 属性缺失:按键分支中,pluck()对缺失的属性会返回null(见 pluck.js 的兜底逻辑),null经parseFloat后为NaN,同样会影响均价。
  • 不修改原集合:带参分支使用新构造的集合完成求值,average_test.js 对此有显式断言。

TypeScript 类型签名

collect.js 自带类型声明 index.d.ts,average与avg的签名如下(index.d.ts):

/** * Alias for the avg() method. */ average<K>(key?: keyof Item | K): number; /** * The avg method returns the average of all items in the collection. */ avg<K>(key?: keyof Item | K): number;

key可选,类型为keyof Item | K,既可以是集合元素的属性名,也可以是任意泛型(配合回调使用时由回调参数推导)。返回值统一为number。需要指出的是,该签名将回调形态也归类在可选key参数下,实际传函数时 TypeScript 会要求你自行断言或使用泛型,这是类型声明对三种调用形态的简化建模。

典型链式场景

average()/avg()通常与过滤、排序、去重等操作组合使用,例如基于 usage.md 的链式风格:

collect(products) .where('price', '>', 299) // 先过滤 .avg('price'); // 再求平均 collect([1, 3, 3, 7]) .unique() // 去重后元素为 [1, 3, 7] .avg(); // 3.6666666666666665

作为 collect.js 核心 API 的一员,average()与avg()完全免费依赖(package 本身即为零依赖的数组/对象包装库),在需要对一组数据进行集中趋势统计时,是比手写reduce循环更简洁、可读性更高的选择。

  • 开发工具

【免费下载链接】collect.js

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

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

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进