es-toolkit 兼容版 minBy 完全指南:从 Lodash 迁移到现代 JavaScript 工具库的最小值查找方案
【免费下载链接】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/compat)中的minBy函数,深入讲解其按条件查找数组最小元素的能力、四种 iteratee 简写形式的内部实现原理,以及从 Lodash 迁移时的性能权衡与最佳实践。读完本文,你将掌握minBy的完整调用形态、边界行为(空数组、NaN、Symbol、null 等),并能基于源码理解兼容版与现代版的差异,在实际项目中做出正确的选型。
为什么需要minBy:按条件求最小元素
JavaScript 原生提供了Math.min(...array)可以直接求数值数组的最小值,但面对对象数组(例如从接口返回的用户列表、商品列表)时,我们往往需要"按某个字段或某个计算结果"来筛选最小元素。minBy正是为这种场景设计的:它接受一个数组和一个取值函数(iteratee),对每个元素计算出一个可比较的值,然后返回对应值最小的那个元素本身,而不是计算后的值。
es-toolkit 在src/array/minBy.ts中提供了现代版实现(推荐日常使用),同时在src/compat/math/minBy.ts提供了与 Lodash 行为完全对齐的兼容版,并可通过es-toolkit/compat子路径导入。两个版本的差异将在后文详述。
基本签名与参数说明
兼容版minBy的 TypeScript 签名如下:
function minBy<T>( items: ArrayLike<T> | null | undefined, iteratee?: ValueIteratee<T> ): T | undefined;| 参数 | 类型 | 说明 |
|---|---|---|
array | ArrayLike<T> \| null \| undefined | 待搜索的数组(也支持类数组对象、字符串等);传入null或undefined时直接返回undefined |
iteratee | ValueIteratee<T>,可选 | 应用于每个元素的函数、属性名、索引或条件对象,默认值为identity(原样返回元素自身) |
| 返回值 | T \| undefined | 基于条件值最小的那个元素;数组为空时返回undefined |
其中ValueIteratee<T>的类型定义位于 src/compat/_internal/ValueIteratee.ts:
export type ValueIteratee<T> = ((value: T) => unknown) | (PropertyKey | [PropertyKey, any] | PartialShallow<T>);也就是说,iteratee 可以是以下四种形式之一:函数、属性名(字符串/数字/Symbol)、二元组[key, value]、部分匹配对象。这也是 Lodash 经典的 "shorthand"(简写)体系。
四种 iteratee 用法详解
1. 函数形式:自定义取值逻辑
传入一个函数,从每个元素中提取用于比较的数值:
import { minBy } from 'es-toolkit/compat'; const people = [ { name: 'John', age: 25 }, { name: 'Jane', age: 30 }, { name: 'Bob', age: 35 }, ]; minBy(people, person => person.age); // 返回: { name: 'John', age: 25 } const numbers = [-1, -2, -3]; minBy(numbers, x => Math.abs(x)); // 返回: -1(绝对值最小的元素)函数形式最灵活,可以表达任意取值逻辑,例如对日期取时间戳、对字符串取长度、对嵌套字段做组合计算等。兼容版在内部会把函数原样透传,不做额外包装(见下文 iteratee 解析逻辑)。
2. 属性名简写:直接按字段取值
传入一个字符串(或数字索引),等价于x => x['age']:
import { minBy } from 'es-toolkit/compat'; minBy(people, 'age'); // 返回: { name: 'John', age: 25 } // 数字索引作用于嵌套数组,取每个子数组的第 N 个元素进行比较 const arrays = [ [1, 2], [3, 4], [0, 5], ]; minBy(arrays, 0); // 按第一个元素比较,返回 [0, 5] minBy(arrays, 1); // 按第二个元素比较,返回 [1, 2]当目标元素本身就是数组时,数字属性名简写尤其有用,例如按坐标数组的横轴或纵轴筛选最小点。
3.[key, value]二元组简写:匹配特定键值对
传入一个二元组[key, value],函数会筛选出满足element[key] === value的元素,再取其中的最小值:
import { minBy } from 'es-toolkit/compat'; const users = [ { name: 'John', age: 25, active: true }, { name: 'Jane', age: 30, active: false }, { name: 'Bob', age: 35, active: true }, ]; // 在所有 active: true 的元素中取最小 minBy(users, ['active', true]); // 返回: { name: 'Jane', age: 30, active: false }注意这里的返回语义是"第一个不满足匹配条件的元素"——因为只有匹配['active', true]的元素(John、Bob)才被纳入比较,而它们都不满足匹配时,结果回退为未匹配集合中的第一个元素。理解这一点需要结合matchesProperty的谓词语义(见源码分析一节)。
4. 部分对象简写:按多个属性匹配
传入一个对象,函数会筛选出"包含该对象所有属性且属性值相等"的元素:
import { minBy } from 'es-toolkit/compat'; minBy(users, { active: true }); // 返回: { name: 'Jane', age: 30, active: false }对象简写等价于 lodash 的_.matches语义:对每个元素做"部分匹配"(partial deep match),匹配成功的元素参与比较,未匹配元素被排除,最终取匹配集合中值最小的那个元素。
边界行为:空值与特殊值的处理
兼容版minBy对空值和不可比较值有一套与 Lodash 对齐的明确规则:
import { minBy } from 'es-toolkit/compat'; // 空数组 / null / undefined 一律返回 undefined minBy([], x => x.a); // => undefined minBy(null); // => undefined minBy(undefined); // => undefined // 数组只有单个元素时返回该元素本身 minBy([40], x => x); // => 40结合 src/compat/math/minBy.spec.ts 中的测试用例,可以确认以下细节:
null/undefined输入:minBy(null)、minBy(undefined)均返回undefined(实现中通过items == null的宽松判空提前返回)。- NaN 值被跳过:
minBy([NaN, 3, 1, 2], x => x)返回1,而不是 NaN;若所有元素取值都是 NaN,则返回undefined。这与现代版src/array/minBy.ts的行为不同(现代版遇到 NaN 会立即返回该元素,对齐Math.min的 NaN 传播语义)。 - Symbol 值被跳过:
minBy([Symbol('a'), 3, 1, 2], x => x)返回1;全部为 Symbol 时返回undefined。 null/undefined取值被跳过:minBy([{ a: undefined }, { a: 5 }, { a: null }], 'a')返回{ a: 5 }——因为null会被强转为 0,若不跳过会错误地成为最小值。- iteratee 对所有元素都取不到可比较值(如
minBy([{ a: 1 }, { a: 2 }], 'b'),b键缺失):返回undefined。 - 无 iteratee 参数时默认
identity:minBy([3, 1, 2])返回1。 - 字符串返回值同样支持比较:
minBy([{ v: 'b' }, { v: 'a' }], item => item.v)返回{ v: 'a' },字符串按字典序比较。 ±Infinity正常参与比较:minBy([{ a: -Infinity }, { a: -Infinity }], o => o.a)返回第一个元素。- Date 对象:
minBy([curr, past], date => date.getTime())返回较早的past。 - 超大数组:测试覆盖了 50 万个元素的数组(
Array.from({ length: 5e5 }, (_, i) => i)),单次线性扫描即可完成。
源码级原理:iteratee 是如何被解析的
兼容版minBy的核心实现位于 src/compat/math/minBy.ts:
export function minBy<T>(items: ArrayLike<T> | null | undefined, iteratee: ValueIteratee<T> = identity): T | undefined { if (items == null) { return undefined; } const array = toArray(items); // 类数组转真数组 if (array.length === 0) { return undefined; } const getValue = iterateeToolkit(iteratee); // 关键:四种形式的统一归一化 let minElement: T | undefined; let min: unknown; for (let i = 0; i < array.length; i++) { const element = array[i]; const current = getValue(element, i, array); if (current == null || Number.isNaN(current) || typeof current === 'symbol') { continue; // 跳过不可比较值 } if (min === undefined || current < (min as number)) { min = current; minElement = element; } } return minElement; }几个值得注意的实现细节:
toArray归一化:ArrayLike<T>(如类数组arguments、字符串、带length的对象)会先通过 src/compat/_internal/toArray.ts 的Array.isArray(value) ? value : Array.from(value)转为真数组,保证循环逻辑统一。- 单次线性扫描:算法是 O(n) 的
for循环,不使用reduce或排序,避免多余开销。 - 跳过语义:
null、NaN、Symbol类型的取值直接continue,这正是前面边界行为的来源;min === undefined的判断用于处理"第一个有效元素"的初始化。
iteratee 四路分发的真相
上面代码中的iterateeToolkit(iteratee)来自 src/compat/util/iteratee.ts,它是四种简写形式统一的"归一化工厂":
export function iteratee( value?: symbol | number | string | object | null | ((...args: any[]) => unknown) ): (...args: any[]) => any { if (value == null) { return identity; // 无参 / null => 恒等函数 } switch (typeof value) { case 'function': { return value as any; // 函数原样返回 } case 'object': { if (Array.isArray(value) && value.length === 2) { return matchesProperty(value[0], value[1]); // 二元组 => matchesProperty } return matches(value); // 普通对象 => matches(部分匹配) } default: { return property(value); // 字符串/数字/Symbol => property 取值 } } }分发规则与ValueIteratee<T>类型定义一一对应:
- 函数→ 原样返回,直接以
(element, index, array)三个参数调用; - 属性名(string / number / symbol)→ 包装为 src/compat/object/property.ts 的
property取值函数,支持'a.b.c'之类的点路径; - 二元组
[key, value]→ 包装为 src/compat/predicate/matchesProperty.ts 的matchesProperty,返回"该元素属性是否等于给定值"的布尔谓词; - 普通对象→ 包装为 src/compat/predicate/matches.ts 的
matches,返回"该元素是否部分匹配给定对象"的布尔谓词。
正是这种"谓词即取值函数"的统一设计,使得minBy(users, ['active', true])这类"先筛选再取最小"的写法成为可能——文档中示例的返回结果是"第一个不满足匹配条件的元素",其根源就在于matchesProperty对不匹配元素返回false(可比较值),而false < true恒成立,导致匹配集合中的元素永远无法成为最小值,结果自然回退到未匹配集合中的首元素。
兼容版 vs 现代版:为什么文档建议优先使用es-toolkit主模块
本函数的官方文档(docs/compat/reference/math/minBy.md)在开头就给出了明确的性能警告:
这个
minBy函数因为 iteratee 处理与类型转换而运行较慢,请改用es-toolkit中更快、更现代的 minBy。
对比两个实现可以直观看到性能差异的来源:
| 维度 | 兼容版(es-toolkit/compat) | 现代版(es-toolkit) |
|---|---|---|
| 源文件 | src/compat/math/minBy.ts | src/array/minBy.ts |
| iteratee 参数 | 支持四种简写形式,需经iteratee()分发归一化 | 仅接受函数,零包装开销 |
| 输入类型 | ArrayLike<T> \| null \| undefined,需toArray转换 | readonly T[],需[T, ...T[]]非空元组约束 |
| 空数组语义 | 宽松判断后返回undefined | 重载签名区分:非空元组必有返回值,空数组返回undefined |
| NaN 语义 | 跳过 NaN,对齐 Lodash | 遇到 NaN 立即返回该元素,对齐Math.min的传播语义 |
| 额外跳过逻辑 | null、Symbol均跳过 | 无 |
现代版的核心循环极为精简(src/array/minBy.ts):初始化min = Infinity,单次遍历中if (Number.isNaN(value)) return element; if (value < min) { ... },没有任何类型分发与跳过逻辑,因此在"只需函数取值"的常见场景下性能明显占优,这也是官方文档推荐优先使用它的原因。
选型建议:
- 新项目、追求性能:从
es-toolkit主模块导入minBy,配合函数形式 iteratee; - 从 Lodash 迁移、需要行为 100% 对齐:从
es-toolkit/compat导入,获得属性名简写、[key, value]简写、对象部分匹配、NaN/Symbol 跳过等全部 Lodash 语义,保证迁移后输出与旧代码完全一致。
在项目中的使用方式与验证
minBy位于兼容模块的数学分类下,可通过以下方式导入:
// 兼容版(本文主题,与 Lodash 行为对齐) import { minBy } from 'es-toolkit/compat'; // 现代版(推荐,性能更优) import { minBy } from 'es-toolkit';两者的行为差异均可在仓库的测试套件中验证:兼容版测试见 src/compat/math/minBy.spec.ts,覆盖了 Date、50 万超大数组、单元素数组、+/-Infinity、null/undefined、NaN、Symbol、字符串比较等 13 组场景,是理解边界语义最直接的参考;现代版在 src/array/minBy.spec.ts 中另有对应测试。
小结
minBy是数组"按条件取最小元素"场景的标准答案。兼容版以 Lodash 兼容性为核心目标,通过iteratee()工厂把函数、属性名、索引、二元组、部分对象五种输入统一归一化,并实现了 NaN/Symbol/null 跳过、类数组转换等完整边界语义;而现代版则以性能为核心,用最精简的循环换取更高执行效率。理解两者的差异后,无论是从 Lodash 平滑迁移,还是在全新项目中追求极致性能,你都能做出恰当的选型。
【免费下载链接】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),仅供参考