Lodash 源码阅读-baseSortedIndex
概述
baseSortedIndex
是 Lodash 内部的一个基础函数,它的主要作用是在一个已排序的数组中找到一个值应该被插入的位置。简单来说,它是一个针对数值类型进行特别优化的二分查找实现。
这个函数主要用于支持几个公开的 API,比如 sortedIndex
和 sortedLastIndex
。它的特别之处在于区分了两条处理路径:
- 一条是针对数值类型的快速路径(性能优化版)
- 另一条是处理复杂情况的通用路径(委托给
baseSortedIndexBy
)
这种设计让函数既高效又灵活,能够处理各种不同的输入情况。
前置学习
依赖函数
baseSortedIndex
依赖以下几个函数和常量:
- isSymbol:检查一个值是否为 Symbol 类型
- identity:一个简单的函数,返回输入值本身
- baseSortedIndexBy:更通用的排序索引查找函数
- HALF_MAX_ARRAY_LENGTH:常量,表示数组最大长度的一半
技术知识
要理解这个函数,你需要掌握以下概念:
- 二分查找:一种在有序数组中查找元素的高效算法,时间复杂度为 O(log n)
- 位运算 :使用
>>>
无符号右移运算进行整数除以 2 的计算,比普通除法更高效 - JavaScript 特殊值:了解 null、NaN、Symbol 等特殊值的处理方式
- 优化技巧:了解如何针对常见情况进行性能优化
源码实现
javascript
function baseSortedIndex(array, value, retHighest) {
var low = 0,
high = array == null ? low : array.length;
if (
typeof value == "number" &&
value === value &&
high <= HALF_MAX_ARRAY_LENGTH
) {
while (low < high) {
var mid = (low + high) >>> 1,
computed = array[mid];
if (
computed !== null &&
!isSymbol(computed) &&
(retHighest ? computed <= value : computed < value)
) {
low = mid + 1;
} else {
high = mid;
}
}
return high;
}
return baseSortedIndexBy(array, value, identity, retHighest);
}
实现思路
baseSortedIndex
的实现非常聪明,它采用了两条处理路径:
-
快速路径(针对数值类型):
- 首先检查目标值是否为数字(不是 NaN)
- 检查数组长度是否在安全范围内
- 如果条件满足,使用优化的二分查找算法直接处理
- 这条路径使用位运算计算中间索引,性能更好
-
通用路径(处理所有其他情况):
- 如果不满足快速路径的条件,调用更通用的
baseSortedIndexBy
函数 - 传入
identity
函数作为迭代器,表示不对元素进行转换
- 如果不满足快速路径的条件,调用更通用的
这种设计让函数能够在处理数值类型时获得最佳性能,同时对于复杂情况也能正确处理。
源码解析
初始化搜索范围
javascript
var low = 0,
high = array == null ? low : array.length;
这段代码设置了搜索的起点和终点:
low = 0
表示从数组开始处搜索high
设为数组长度,如果数组为 null 或 undefined,则设为 0- 这样确保了函数能安全处理空数组或者 null/undefined 输入
快速路径条件检查
javascript
if (
typeof value == "number" &&
value === value &&
high <= HALF_MAX_ARRAY_LENGTH
) {
// 快速路径...
}
这段代码检查是否能走快速路径:
typeof value == "number"
- 目标值必须是数字类型value === value
- 目标值不能是 NaN(因为 NaN !== NaN)high <= HALF_MAX_ARRAY_LENGTH
- 数组长度不能超过一半的最大数组长度
这里的 HALF_MAX_ARRAY_LENGTH
大约是 21 亿(具体值为 2^30 或 1073741823),这个限制是为了防止计算中间索引时可能发生的整数溢出。在 JavaScript 中,数字使用 IEEE 754 标准的双精度浮点数表示,当两个很大的数相加时(如数组长度接近最大值时的 low + high),可能会导致精度丢失。通过限制数组长度不超过 HALF_MAX_ARRAY_LENGTH
,确保了 low + high
的计算结果不会超出安全整数范围,从而避免了在二分查找过程中可能出现的索引计算错误。这是一种防御性编程的实践,保证了算法在处理大型数组时的正确性。
二分查找循环
javascript
while (low < high) {
var mid = (low + high) >>> 1,
computed = array[mid];
if (
computed !== null &&
!isSymbol(computed) &&
(retHighest ? computed <= value : computed < value)
) {
low = mid + 1;
} else {
high = mid;
}
}
这是二分查找的核心循环:
(low + high) >>> 1
计算中间索引,使用无符号右移运算(相当于除以 2 并取整)- 然后检查数组中间元素的条件:
- 不能是 null
- 不能是 Symbol 类型
- 根据
retHighest
参数决定使用哪种比较运算符:- 当
retHighest
为 false 时,使用<
查找第一个不小于目标值的位置 - 当
retHighest
为 true 时,使用<=
查找最后一个不大于目标值的位置
- 当
- 根据比较结果调整搜索范围:
- 如果条件满足,在右半部分继续搜索 (
low = mid + 1
) - 否则,在左半部分继续搜索 (
high = mid
)
- 如果条件满足,在右半部分继续搜索 (
通用路径
javascript
return baseSortedIndexBy(array, value, identity, retHighest);
如果不满足快速路径条件,就使用这个通用路径:
- 调用
baseSortedIndexBy
函数处理复杂情况 - 传入
identity
函数,它简单地返回输入值本身 - 这样就能处理非数值类型、NaN 和超大数组等情况
总结
baseSortedIndex
函数虽然看起来不大,但设计得非常巧妙,从中我们可以学到很多:
-
性能优化思路
- 区分快速路径和通用路径,针对常见情况优化
- 使用位运算代替普通数学运算,提高性能
- 对特殊情况(如空数组)进行短路处理
-
代码复用技巧
- 不重复实现复杂逻辑,而是复用现有函数
- 让专门的函数处理专门的事情
- 为多个公开 API 提供统一的底层实现
-
健壮的边界处理
- 安全处理 null 和 undefined 输入
- 正确处理数值范围限制,防止整数溢出
- 应对特殊值(如 NaN、Symbol)的比较逻辑
-
灵活的设计模式
- 使用参数
retHighest
控制行为,一个函数满足两种需求 - 优先使用快速路径,条件不满足时回退到通用路径
- 清晰的职责分离,提高代码可维护性
- 使用参数