Lodash 源码阅读-baseSortedIndexBy
概述
baseSortedIndexBy
是 Lodash 内部的一个基础函数,它的主要作用是在已排序的数组中找到一个值应该被插入的位置。简单来说,它是一个增强版的二分查找实现,具有以下特点:
- 支持通过迭代器函数转换数组元素后再比较
- 能够处理各种特殊值(NaN、null、undefined、Symbol 等)
- 可以根据需要返回相等元素的最低位置或最高位置
- 作为多个公开 API(如
sortedIndexBy
、sortedLastIndexBy
等)的底层实现
与普通二分查找相比,这个函数处理了更多的边缘情况,使其在各种复杂场景下都能正确工作。
重要说明 :该函数假设输入的数组是按升序排列的。也就是说,数组元素从小到大排列,这是二分查找算法的基本前提。如果输入的数组不是升序排列的,函数将无法正确工作。所有基于此函数的 API(如sortedIndexBy
、sortedLastIndexBy
)也都要求输入数组是升序排列的。
前置学习
依赖函数
baseSortedIndexBy
依赖以下几个函数:
- isSymbol:检查一个值是否为 Symbol 类型
- nativeFloor :就是原生的
Math.floor
函数,用于计算中间索引 - nativeMin :就是原生的
Math.min
函数,用于确保返回值不超过数组最大长度 - MAX_ARRAY_INDEX:常量,表示数组最大索引值
技术知识
要理解这个函数,你需要掌握以下知识点:
- 二分查找算法:在有序数组中快速查找元素的方法,时间复杂度为 O(log n)
- JavaScript 中的特殊值比较:了解 NaN、null、undefined、Symbol 等特殊值的比较规则
- 迭代器模式:通过函数转换元素后再进行比较
- 边界情况处理:处理空数组、特殊值和索引边界等情况
源码实现
javascript
function baseSortedIndexBy(array, value, iteratee, retHighest) {
var low = 0,
high = array == null ? 0 : array.length;
if (high === 0) {
return 0;
}
value = iteratee(value);
var valIsNaN = value !== value,
valIsNull = value === null,
valIsSymbol = isSymbol(value),
valIsUndefined = value === undefined;
while (low < high) {
var mid = nativeFloor((low + high) / 2),
computed = iteratee(array[mid]),
othIsDefined = computed !== undefined,
othIsNull = computed === null,
othIsReflexive = computed === computed,
othIsSymbol = isSymbol(computed);
if (valIsNaN) {
var setLow = retHighest || othIsReflexive;
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined);
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
} else if (valIsSymbol) {
setLow =
othIsReflexive &&
othIsDefined &&
!othIsNull &&
(retHighest || !othIsSymbol);
} else if (othIsNull || othIsSymbol) {
setLow = false;
} else {
setLow = retHighest ? computed <= value : computed < value;
}
if (setLow) {
low = mid + 1;
} else {
high = mid;
}
}
return nativeMin(high, MAX_ARRAY_INDEX);
}
实现思路
baseSortedIndexBy
的实现逻辑可以分为以下几个步骤:
- 初始化搜索范围:设置初始搜索区间为整个数组(从 0 到数组长度)
- 处理空数组:如果数组为空,直接返回 0
- 转换目标值:使用迭代器函数转换要查找的值,并检查它是否为特殊值
- 二分查找过程 :
- 计算中间位置
- 转换中间位置的元素
- 根据值的类型和
retHighest
参数决定如何调整搜索范围 - 不断缩小搜索范围直到找到正确位置
- 返回结果:确保返回值不超过数组最大索引
整个实现的核心在于如何处理特殊值的比较,这部分逻辑比较复杂,但能确保函数在各种边缘情况下都能正确工作。
记住,整个算法都基于数组已经是升序排列的假设。如果数组是降序排列或无序的,查找结果将不准确。
源码解析
初始化和边界检查
javascript
var low = 0,
high = array == null ? 0 : array.length;
if (high === 0) {
return 0;
}
这段代码做了两件事:
- 设置搜索范围:
low = 0
(开始位置),high = array.length
(结束位置) - 如果数组为空(长度为 0),直接返回 0
这里使用array == null
同时检查null
和undefined
,这是一个常见的 JavaScript 技巧。
目标值转换和类型检查
javascript
value = iteratee(value);
var valIsNaN = value !== value,
valIsNull = value === null,
valIsSymbol = isSymbol(value),
valIsUndefined = value === undefined;
这部分代码:
- 使用迭代器函数转换目标值
- 检查转换后的值是什么类型:
valIsNaN
:是否为 NaN(利用 NaN 不等于自身的特性)valIsNull
:是否为 nullvalIsSymbol
:是否为 Symbol 类型valIsUndefined
:是否为 undefined
这些标记将在后续的比较逻辑中使用。
二分查找主循环
javascript
while (low < high) {
var mid = nativeFloor((low + high) / 2),
computed = iteratee(array[mid]),
othIsDefined = computed !== undefined,
othIsNull = computed === null,
othIsReflexive = computed === computed,
othIsSymbol = isSymbol(computed);
if (valIsNaN) {
var setLow = retHighest || othIsReflexive;
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined);
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
} else if (valIsSymbol) {
setLow =
othIsReflexive &&
othIsDefined &&
!othIsNull &&
(retHighest || !othIsSymbol);
} else if (othIsNull || othIsSymbol) {
setLow = false;
} else {
setLow = retHighest ? computed <= value : computed < value;
}
if (setLow) {
low = mid + 1;
} else {
high = mid;
}
}
这是标准的二分查找循环,让我们详细解释每个变量的含义:
low
和high
:当前搜索区间的下界和上界索引mid
:搜索区间的中间索引,使用nativeFloor
(即Math.floor
)向下取整computed
:中间位置元素经过迭代器函数转换后的值othIsDefined
:检查computed
是否不是undefined
,如果是则为true
othIsNull
:检查computed
是否为null
,如果是则为true
othIsReflexive
:检查computed
是否等于自身,这是检测 NaN 的技巧(NaN !== NaN)othIsSymbol
:检查computed
是否为 Symbol 类型,使用isSymbol
函数判断setLow
:决定是调整下界还是上界的标志变量,在复杂比较逻辑中设置
这些变量用于处理不同类型的边界情况:
othIsReflexive
专门用来检测 NaN:如果computed
是 NaN,则othIsReflexive
为false
othIsDefined
和othIsNull
用于处理undefined
和null
这些特殊值othIsSymbol
用于处理 Symbol 类型,因为 Symbol 值不能与其他类型直接比较
在每次循环迭代中:
- 如果
setLow
为true
,说明需要在右半部分继续查找,所以将low
设置为mid + 1
- 如果
setLow
为false
,说明需要在左半部分继续查找,所以将high
设置为mid
这个循环会一直执行,直到 low
等于 high
,此时找到了目标值应该插入的位置。
简单来说就是
- 计算中间位置
mid
- 使用迭代器函数转换中间元素
- 检查中间元素的类型(与目标值类似)
- 根据比较结果调整搜索范围:
- 如果需要在右半部分搜索,则
low = mid + 1
- 否则在左半部分搜索,
high = mid
- 如果需要在右半部分搜索,则
复杂的比较逻辑
javascript
if (valIsNaN) {
var setLow = retHighest || othIsReflexive;
} else if (valIsUndefined) {
setLow = othIsReflexive && (retHighest || othIsDefined);
} else if (valIsNull) {
setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
} else if (valIsSymbol) {
setLow =
othIsReflexive &&
othIsDefined &&
!othIsNull &&
(retHighest || !othIsSymbol);
} else if (othIsNull || othIsSymbol) {
setLow = false;
} else {
setLow = retHighest ? computed <= value : computed < value;
}
这是函数最复杂的部分,处理各种特殊值的比较。让我们详细解析每个条件分支处理的具体场景:
-
目标值为 NaN 的情况:
javascriptif (valIsNaN) { var setLow = retHighest || othIsReflexive; }
- 场景:当我们要查找 NaN 应该插入的位置
- 处理策略:
- 如果
retHighest
为 true(寻找最后位置),则setLow
为 true - 如果中间元素不是 NaN(
othIsReflexive
为 true),则setLow
为 true - 这意味着 NaN 会被放在所有非 NaN 值之后,或者是在最后一个 NaN 之后(如果
retHighest
为 true)
- 如果
- 举例:在 [1, NaN, 3] 中寻找 NaN 的插入位置
- 当
retHighest
为 false,结果为索引 1 - 当
retHighest
为 true,结果为索引 2(最后一个 NaN 后面)
- 当
-
目标值为 undefined 的情况:
javascriptelse if (valIsUndefined) { setLow = othIsReflexive && (retHighest || othIsDefined); }
- 场景:当我们要查找 undefined 应该插入的位置
- 处理策略:
- 首先,中间元素不能是 NaN(
othIsReflexive
必须为 true) - 其次,如果
retHighest
为 true 或中间元素不是 undefined(othIsDefined
为 true),则setLow
为 true - 这意味着 undefined 会被放在所有 NaN 之前,以及所有非 undefined 值之后
- 首先,中间元素不能是 NaN(
- 举例:在 [1, undefined, 3] 中寻找 undefined 的插入位置
- 当
retHighest
为 false,结果为索引 1 - 当
retHighest
为 true,结果为索引 2(最后一个 undefined 后面)
- 当
-
目标值为 null 的情况:
javascriptelse if (valIsNull) { setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); }
- 场景:当我们要查找 null 应该插入的位置
- 处理策略:
- 中间元素不能是 NaN(
othIsReflexive
为 true) - 中间元素不能是 undefined(
othIsDefined
为 true) - 如果
retHighest
为 true 或中间元素不是 null(!othIsNull
为 true),则setLow
为 true - 这意味着 null 会被放在所有 NaN 和 undefined 之前,以及所有非 null 值之后
- 中间元素不能是 NaN(
- 举例:在 [1, null, 3] 中寻找 null 的插入位置
- 当
retHighest
为 false,结果为索引 1 - 当
retHighest
为 true,结果为索引 2(最后一个 null 后面)
- 当
-
目标值为 Symbol 的情况:
javascriptelse if (valIsSymbol) { setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); }
- 场景:当我们要查找 Symbol 应该插入的位置
- 处理策略:
- 中间元素不能是 NaN(
othIsReflexive
为 true) - 中间元素不能是 undefined(
othIsDefined
为 true) - 中间元素不能是 null(
!othIsNull
为 true) - 如果
retHighest
为 true 或中间元素不是 Symbol(!othIsSymbol
为 true),则setLow
为 true - 这意味着 Symbol 会被放在所有 NaN、undefined 和 null 之前,以及所有非 Symbol 值之后
- 中间元素不能是 NaN(
- 举例:在 [1, Symbol(), 3] 中寻找 Symbol 的插入位置
- 当
retHighest
为 false,结果为索引 1 - 当
retHighest
为 true,结果为索引 2(最后一个 Symbol 后面)
- 当
-
中间元素为特殊值的情况:
javascriptelse if (othIsNull || othIsSymbol) { setLow = false; }
- 场景:当目标值是普通值,但中间元素是 null 或 Symbol
- 处理策略:
- 直接设置
setLow
为 false,表示在左半部分继续查找 - 这意味着普通值会被放在所有 null 和 Symbol 之前
- 直接设置
- 举例:在 [null, Symbol(), 3] 中寻找 2 的插入位置
- 结果为索引 0,因为普通数值应该排在 null 和 Symbol 之前
-
普通值的情况:
javascriptelse { setLow = retHighest ? computed <= value : computed < value; }
- 场景:当目标值和中间元素都是普通值
- 处理策略:
- 当
retHighest
为 false 时,使用<
比较,查找第一个不小于目标值的位置 - 当
retHighest
为 true 时,使用<=
比较,查找最后一个不大于目标值的位置
- 当
- 举例:在升序数组 [1, 2, 2, 3] 中寻找 2 的插入位置
- 当
retHighest
为 false,结果为索引 1(第一个 2) - 当
retHighest
为 true,结果为索引 3(最后一个 2 后面)
- 当
这种复杂的比较逻辑确保了各种 JavaScript 值类型都有一个确定的排序顺序:
- 普通值(按自然升序)
- Symbol 值
- null 值
- undefined 值
- NaN 值
并且,对于相等的值,可以根据 retHighest
参数决定是返回第一个位置还是最后一个位置,这种灵活性使得函数可以支持多种查找需求。例如,
- 当
retHighest
为false
时,函数查找第一个不小于目标值的位置(用于sortedIndexBy
) - 当
retHighest
为true
时,函数查找最后一个不大于目标值的位置(用于sortedLastIndexBy
)
简单来说就是
- NaN 值的处理 :
- 当目标值是 NaN 时,根据
retHighest
和中间元素是否为 NaN 决定搜索方向
- 当目标值是 NaN 时,根据
- undefined 值的处理 :
- 当目标值是 undefined 时,如果中间元素不是 NaN,且满足特定条件,则在右侧搜索
- null 值的处理 :
- 当目标值是 null 时,如果中间元素既不是 NaN 也不是 undefined,且满足特定条件,则在右侧搜索
- Symbol 值的处理 :
- 当目标值是 Symbol 时,如果中间元素不是 NaN、undefined 或 null,且满足特定条件,则在右侧搜索
- 中间元素为特殊值 :
- 如果中间元素是 null 或 Symbol,则在左侧搜索
- 普通值的比较 :
- 对于普通值,使用
<
或<=
比较,取决于retHighest
参数
- 对于普通值,使用
返回结果
javascript
return nativeMin(high, MAX_ARRAY_INDEX);
最后返回找到的位置,使用nativeMin
确保不超过最大数组索引(MAX_ARRAY_INDEX
),这是一个安全措施。
总结
baseSortedIndexBy
是 Lodash 中一个设计精巧的内部函数,通过几点我们可以学习到很多:
-
算法优化
- 使用二分查找将时间复杂度从 O(n)降低到 O(log n)
- 短路处理空数组,避免不必要的计算
-
健壮性设计
- 全面处理 JavaScript 中的特殊值比较
- 安全地处理各种边界情况
- 防止返回值超出有效范围
-
API 灵活性
- 支持自定义转换函数,适应不同需求
- 通过参数控制返回相等元素的最低或最高索引
- 为多个公开 API 提供统一实现,减少代码重复
-
代码可读性
- 清晰的变量命名(虽然逻辑复杂)
- 将比较逻辑按不同类型分类处理