es-toolkit/fp 的 maxBy:在函数式管道中选出最大值的正确姿势
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
本篇指南聚焦 es-toolkit 函数式编程子模块es-toolkit/fp中的maxBy操作符:它创建一个"返回计算得分最大元素"的函数,专用于与pipe组合实现数据流式处理。读完本文,你将掌握 fp 版maxBy的调用约定、参数与返回值类型、空数组与 NaN 等边界行为,以及它与普通版maxBy在何时该用哪一个的取舍。
概述:一个为 pipe 而生的函数
es-toolkit/fp是一套data-last(数据置后)风格的函数式编程 API。所谓>const result = pipe(array, maxBy(getValue));
它本身不直接接收数组,而是接收一个用于计算"得分"的getValue函数,返回一个把readonly T[]映射为最大元素(或undefined)的函数。这与普通版maxBy(items, getValue)的参数顺序(数据在前、配置在后)正好相反。
使用法:管道中的一行调用
在管道中,maxBy会对流入数组的每个元素调用getValue,并返回计算结果最大的那个元素;若数组为空,则返回undefined:
import { maxBy, pipe } from 'es-toolkit/fp'; pipe( [{ score: 10 }, { score: 30 }, { score: 20 }], maxBy(item => item.score) ); // => { score: 30 }其行为可以概括为:
- 对数组每个元素调用
getValue,取其数值结果; - 返回数值结果最大的那个原始元素(而不是得分本身);
- 数组为空时返回
undefined。
参数
getValue((item: T) => number):返回用于比较的数值的函数。
值得补充的是,虽然文档以(item: T) => number简写,但源码中的完整签名还向getValue传递了索引与数组本身两个附加参数。见 fp 实现:
getValue: (element: T, index: number, array: readonly T[]) => number这意味着在管道里同样可以写出依赖下标或整数组的得分函数,例如maxBy((item, index) => item.value + index)。
返回值
(array: readonly T[]) => T | undefined:一个把readonly T[]映射为最大元素或undefined的函数。注意getValue返回的是 number 类型,这是与处理 bigint 等类型的其他实现的一个重要区别。
空数组与非空数组:两个重载
fp 版maxBy在类型层面为调用者提供了两组重载(见 src/fp/array/maxBy.ts):
// 非空数组(元组类型):保证返回 T export function maxBy<T>( getValue: (element: T, index: number, array: readonly T[]) => number ): (array: readonly [T, ...T[]]) => T; // 一般数组:可能为空,返回 T | undefined export function maxBy<T>( getValue: (element: T, index: number, array: readonly T[]) => number ): (array: readonly T[]) => T | undefined;也就是说,当你传入的数组在类型上被标注为非空(如readonly [T, ...T[]])时,TypeScript 能推断出结果必然存在;而普通T[]数组则可能得到undefined,编译器会强制你处理空数组分支。这是 es-toolkit 在类型安全上的细节设计,从源码的重载签名可以直接确认。
底层实现:它只是普通 maxBy 的柯里化包装
fp 版maxBy的完整实现非常精简(src/fp/array/maxBy.ts#L42-L48):
export function maxBy<T>( getValue: (element: T, index: number, array: readonly T[]) => number ): (array: readonly T[]) => T | undefined { return function (array: readonly T[]): T | undefined { return maxByToolkit(array, getValue); }; }它只是把普通版maxBy(从../../array/maxBy.ts导入)做了一次"数据后置"的柯里化封装:先捕获getValue,返回的闭包在收到数组后再以maxByToolkit(array, getValue)调用真正的实现。因此两者的算法与边界行为完全一致,fp 版不引入任何额外的排序或复制逻辑,保持了 es-toolkit 一贯的轻量特性。
核心算法:单次遍历 + 从 -Infinity 起步
普通版maxBy的算法位于 src/array/maxBy.ts#L74-L100,要点如下:
if (items.length === 0) { return undefined; } let maxElement = items[0]; let max = -Infinity; for (let i = 0; i < items.length; i++) { const element = items[i]; const value = getValue(element, i, items); if (Number.isNaN(value)) { return element; } if (value > max) { max = value; maxElement = element; } } return maxElement;几个值得注意的实现细节:
- 单次线性遍历,时间复杂度 O(n),不产生中间数组;
- 初始最大值设为
-Infinity,保证任何有限数都能被选中;由于比较用的是严格>,并列最大值时返回先出现的元素(首个最大元素); - NaN 传播:一旦某个元素的得分是
NaN,立即返回该元素本身,这与原生Math.max的 NaN 行为一致——这一点在测试中有专门用例覆盖(src/array/maxBy.spec.ts#L38-L42),无论 NaN 出现在数组的什么位置都会传播; getValue会被调用时传入(element, index, array)三个参数,对应测试中的 index 与 array 用例(src/array/maxBy.spec.ts#L44-L54)。
fp 版与普通版:何时用哪个
maxBy在 es-toolkit 中存在两个入口,使用建议非常明确:
| 场景 | 推荐 API | 导入路径 |
|---|---|---|
| 普通命令式代码,直接对数组求最大值 | 普通版maxBy | es-toolkit/array(或es-toolkit) |
用pipe组合多个变换的流水线 | fp 版maxBy | es-toolkit/fp |
普通代码中,直接用数据在前的普通版更直观:
import { maxBy } from 'es-toolkit/array'; const people = [ { name: 'john', age: 30 }, { name: 'jane', age: 28 }, { name: 'joe', age: 26 }, ]; maxBy(people, person => person.age); // => { name: 'john', age: 30 }而当你想把"取最大值"作为流水线的一环、与其他操作符(如filter、map、sortBy等)串联时,则应该使用 fp 版,让pipe把前一步的输出作为数组喂给maxBy。普通版的完整用法与更多示例可参考 普通版 maxBy 文档。
在 pipe 流水线中的实战组合
把maxBy放进管道,可以写出可读性很强的声明式数据处理流程。例如:从一批商品中筛选出有效商品,再找出评分最高者:
import { pipe, filter, maxBy } from 'es-toolkit/fp'; const products = [ { name: 'A', price: 100, rating: 4.2 }, { name: 'B', price: 200, rating: 4.8 }, { name: 'C', price: 50, rating: 3.9 }, ]; pipe( products, filter(product => product.price > 0), maxBy(product => product.rating) ); // => { name: 'B', price: 200, rating: 4.8 }注意一个细节:fp 版的maxBy本身不是"惰性"操作符(它没有lazy属性),因此它属于pipe中的普通求值函数,会在其位置被逐个应用到当前数据上。与之相对,map、filter、take等惰性函数在连续出现时会被pipe融合成单趟短路遍历。这不影响正确性,只是说明maxBy作为聚合操作符,天然需要消费完整数组后才能给出答案。
对于对称的"取最小得分"需求,fp 版还提供了minBy,调用约定与maxBy完全一致,只是返回得分最小的元素。
小结
- fp 版
maxBy(getValue)返回一个函数,供pipe流水线使用,data-last 风格与普通版相反; - 对每个元素调用
getValue取数值得分,返回得分最大的原始元素,空数组返回undefined; - 类型重载区分非空数组与可能为空的数组,非空元组类型下结果被推断为必存在;
- 底层就是对普通版
maxBy的柯里化包装(src/fp/array/maxBy.ts),算法为单次遍历、初始值-Infinity、并列取首个、NaN 传播,与Math.max语义一致; - 普通代码用
es-toolkit/array的maxBy,组合变换时用es-toolkit/fp的maxBy。
相关实现与测试:fp 封装 src/fp/array/maxBy.ts、普通版实现 src/array/maxBy.ts、fp 测试 src/fp/array/maxBy.spec.ts、普通版测试 src/array/maxBy.spec.ts。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考