es-toolkit 的 sortedIndexBy 详解:基于变换函数的有序数组插入位置二分查找 es-toolkit 的 sortedIndexBy 详解基于变换函数的有序数组插入位置二分查找【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit导读sortedIndexBy是 es-toolkit 兼容 lodash 风格 APIes-toolkit/compat中用于有序数组查找的数组工具函数。它解决的是这样一个经典问题在一个已经排好序的数组中将一个值插入到哪个最小下标才能继续保持数组的有序性——且插入前允许先通过一个变换函数iteratee对数组元素和待插入值做投影比较。本文将以 sortedIndexBy 参考文档 为核心结合 sortedIndexBy.ts 的源码实现与 sortedIndexBy.spec.ts 测试用例完整讲解它的用法、参数、边界语义、二分查找底层原理以及与sortedIndex、sortedLastIndexBy等兄弟函数的关系。一、sortedIndexBy 是什么sortedIndexBy是sortedIndex的增强版本二者都通过二分查找确定插入下标区别在于sortedIndexBy额外接受一个iteratee变换函数先对数组中的每个元素以及待插入的 value分别执行变换再用变换后的结果进行比较排序。函数签名如下const index sortedIndexBy(array, value, iteratee);从 es-toolkit 的类型定义sortedIndexBy.ts可以看到iteratee 的完整类型为type PropertyName string | number | symbol; type IterateeT, R ((value: T) R) | PropertyName | [PropertyName, any] | PartialT;也就是说iteratee 既可以是函数也可以是属性名、[属性名, 属性值] 二元组甚至部分对象Partial object。这四种形态正是 lodash 风格速记迭代器shorthand iteratee的体现。二、基本用法与代码示例文档给出了三种典型使用场景这里完整继承并逐一展开。场景 1按属性名property shorthand在对象数组中查找import { sortedIndexBy } from es-toolkit/compat; // 在按 x 属性排序的对象数组中确定 { x: 4 } 应插入的最小下标 const objects [{ x: 4 }, { x: 5 }]; sortedIndexBy(objects, { x: 4 }, x); // Returns 0这里x作为属性名速记等价于对每个元素取element.x对 value 取value.x。[4, 5]中插入4最小的合法下标是0插在4之前因此返回0。场景 2使用函数变换// 使用函数对元素和值做变换 const numbers [10, 20, 30]; sortedIndexBy(numbers, 25, n n); // Returns 2传入恒等函数n n时行为退化为sortedIndex在[10, 20, 30]中插入25应插入在30之前即下标2。场景 3使用属性-值数组速记// 使用属性-值数组变换 const users [{ name: alice }, { name: bob }]; sortedIndexBy(users, { name: bob }, [name, bob]); // Returns 1 (equivalent to inserting true into [false, true])[name, bob]是一个[属性名, 属性值]二元组它会把每个元素变换成name bob的布尔值[{ name: alice }, { name: bob }]变换后是[false, true]待插入值{ name: bob }变换后是true。在[false, true]中插入true并保持有序最小下标为1插入到第一个true之前因此返回1。空数组与 null/undefined 的兜底当传入null或undefined数组时函数直接返回0import { sortedIndexBy } from es-toolkit/compat; sortedIndexBy(null, { x: 1 }, x); // 0 sortedIndexBy(undefined, { x: 1 }, x); // 0这一点与sortedIndex一致在源码 sortedIndexBy.ts 中通过isNil判断实现if (isNil(array) || array.length 0) { return 0; }注意当数组为空数组时同样直接返回0且此时不会调用 iteratee——测试用例 sortedIndexBy.spec.ts 用在 iteratee 中抛异常的方式验证了这一点。三、参数与返回值参数类型说明arrayArrayLikeT \| null \| undefined已排序的数组。如果传入未排序数组结果可能不正确这一点在文档中明确强调也是使用该函数的前提约束valueT待插入的值iteratee可选((value: T) R) \| PropertyName \| [PropertyName, any] \| PartialT变换函数、属性名或属性-值数组作用于每个元素和 value 本身缺省时默认为恒等函数identity返回值number—— 插入 value 后仍能保持数组有序的最小下标。当array为null或undefined时返回0。关于默认 iteratee源码 sortedIndexBy.ts 中写明了默认值为identityiteratee: IterateeT, R identity,测试 sortedIndexBy.spec.ts 也专门验证了省略 iteratee 时行为与sortedIndex等价const numbers [10, 30, 50]; sortedIndexBy(numbers, 40); // 2四、iteratee 的四种形态与解析原理为什么x、[name, bob]这种写法能直接作为变换函数使用秘密在于源码内部调用了 iteratee 工具函数。该函数负责把各种速记形式统一转换为真正的比较函数传入形态转换结果函数原样返回直接调用属性名string/number/symbol生成property(key)取值函数例如x→obj obj.x[属性名, 属性值]二元组生成matchesProperty匹配函数例如[name, bob]→obj obj.name bob部分对象生成matches匹配函数判断元素是否匹配该对象无参数 /null返回恒等函数identity在 iteratee.ts 中可以看到其分派逻辑typeof value function直接返回typeof value object时若为长度为 2 的数组则走matchesProperty否则走matches其余字符串、数字、symbol走property。在sortedIndexBy的调用链中这个解析只发生一次然后分别作用于 value 和每个数组元素sortedIndexBy.tsconst iterateeFunction iterateeToolkit(iteratee); const transformedValue iterateeFunction(value);测试用例 sortedIndexBy.spec.ts 还验证了 iteratee 的调用参数——它只接收value这一个参数与 lodash 语义一致。五、底层实现二分查找与特殊值处理sortedIndexBy的核心是一个标准的查找左边界lower bound二分查找。源码 sortedIndexBy.ts 的主循环如下while (low high) { let setLow: boolean; const mid Math.floor((low high) / 2); const computed iterateeFunction(array[mid]); // ... 特殊值判定 ... if (setLow) { low mid 1; } else { high mid; } } return Math.min(high, MAX_ARRAY_INDEX);比较基准computed transformedValue成立时把下界抬高low mid 1否则收窄上界high mid循环结束时low high即为最小插入下标。整个算法时间复杂度为 O(log n)每次迭代只调用一次 iteratee。为了与 lodash 行为完全对齐实现中还对以下特殊变换值做了精细处理sortedIndexBy.tsNaNvalIsNaN为真时只要中间值不是 NaN 就下移保证 NaN 总是插入到末尾区域undefined要求中间值自反非 NaN且已定义时才下移null要求中间值非 NaN、已定义且非 null 时才下移Symbol与 null 类似需要中间值非 NaN、已定义、非 null 且非 Symbol 时才下移中间值为 null 或 Symbol一律不下移setLow false从而把这些特殊值稳定地排在最前面。此外返回值被限制在MAX_ARRAY_INDEX4294967295 - 1以内避免在超长数组上产生越界下标。对应地测试 sortedIndexBy.spec.ts 构造了长度达到Math.ceil(MAX_ARRAY_LENGTH / 2)与MAX_ARRAY_LENGTH的超大稀疏数组并断言二分查找步数稳定在 3233 次之间即 O(log n) 的对数上界同时验证了NaN、undefined的插入位置符合预期。六、与 sortedIndex、sortedLastIndexBy 的关系在 es-toolkit/compat 中这几个函数构成一个完整家族导出见 compat.tssortedIndex不带 iteratee 的朴素版本。它针对数值型 value 走了一条快速路径(low high) 1位运算取中点其余情况会直接委托给sortedIndexBy(array, value, value value)见 sortedIndex.tssortedIndexBy本文主角带 iteratee 的通用版本sortedLastIndexBy求最大插入下标upper bound。它的实现极为简洁——复用sortedIndexBy的内部参数retHighestsortedLastIndexBy.ts// ts-expect-error return sortedIndexBy(array, value, iteratee, true);retHighest为true时比较方向反转computed transformedValue从而返回最右侧的插入位置。这也解释了为什么sortedIndexBy的重载签名中把retHighest作为内部可选参数保留。七、性能说明与基准测试需要特别提醒的是文档开头的警告框明确建议在性能敏感场景下应直接实现更快的二分查找与变换函数而不是使用本函数。原因是sortedIndexBy为了兼容 lodash 的复杂 iteratee 形态函数 / 属性名 / 属性值二元组 / 部分对象以及 NaN、null、undefined、Symbol 等特殊值语义引入了额外的类型转换与多次判定整体运行速度较慢。仓库为此提供了对照基准测试 benchmarks/performance/sortedIndexBy.bench.ts在 100 万规模的数组上同时压测es-toolkit/compat的sortedIndexBy与 lodash 的同名函数覆盖v.x属性变换以及NaN、undefined、null等特殊值输入用于持续追踪兼容层实现的性能表现。如果你的场景只是普通数值数组优先考虑sortedIndex或自行编写针对性的二分查找。八、小结与使用建议综合文档与源码使用sortedIndexBy时有几点值得牢记前提是有序数组函数不会检查或排序传入未排序数组会得到错误下标iteratee 支持四种形态函数、属性名、[属性名, 属性值]二元组、部分对象缺省时为恒等函数边界行为已对齐 lodashnull/undefined/空数组返回0NaN、null、undefined、Symbol 有专门的插入排序语义性能敏感场景慎用如需极致性能建议按文档警告直接手写二分查找。如需进一步验证行为可运行仓库中 sortedIndexBy.spec.ts 对应的测试用例如需查看函数在实际调用链中的导出位置可查阅 compat.ts。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考