Lodash源码阅读-baseSortedIndexBy

Lodash 源码阅读-baseSortedIndexBy

概述

baseSortedIndexBy 是 Lodash 内部的一个基础函数,它的主要作用是在已排序的数组中找到一个值应该被插入的位置。简单来说,它是一个增强版的二分查找实现,具有以下特点:

  • 支持通过迭代器函数转换数组元素后再比较
  • 能够处理各种特殊值(NaN、null、undefined、Symbol 等)
  • 可以根据需要返回相等元素的最低位置或最高位置
  • 作为多个公开 API(如sortedIndexBysortedLastIndexBy等)的底层实现

与普通二分查找相比,这个函数处理了更多的边缘情况,使其在各种复杂场景下都能正确工作。

重要说明 :该函数假设输入的数组是按升序排列的。也就是说,数组元素从小到大排列,这是二分查找算法的基本前提。如果输入的数组不是升序排列的,函数将无法正确工作。所有基于此函数的 API(如sortedIndexBysortedLastIndexBy)也都要求输入数组是升序排列的。

前置学习

依赖函数

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 的实现逻辑可以分为以下几个步骤:

  1. 初始化搜索范围:设置初始搜索区间为整个数组(从 0 到数组长度)
  2. 处理空数组:如果数组为空,直接返回 0
  3. 转换目标值:使用迭代器函数转换要查找的值,并检查它是否为特殊值
  4. 二分查找过程
    • 计算中间位置
    • 转换中间位置的元素
    • 根据值的类型和retHighest参数决定如何调整搜索范围
    • 不断缩小搜索范围直到找到正确位置
  5. 返回结果:确保返回值不超过数组最大索引

整个实现的核心在于如何处理特殊值的比较,这部分逻辑比较复杂,但能确保函数在各种边缘情况下都能正确工作。

记住,整个算法都基于数组已经是升序排列的假设。如果数组是降序排列或无序的,查找结果将不准确。

源码解析

初始化和边界检查

javascript 复制代码
var low = 0,
  high = array == null ? 0 : array.length;
if (high === 0) {
  return 0;
}

这段代码做了两件事:

  • 设置搜索范围:low = 0(开始位置),high = array.length(结束位置)
  • 如果数组为空(长度为 0),直接返回 0

这里使用array == null同时检查nullundefined,这是一个常见的 JavaScript 技巧。

目标值转换和类型检查

javascript 复制代码
value = iteratee(value);
var valIsNaN = value !== value,
  valIsNull = value === null,
  valIsSymbol = isSymbol(value),
  valIsUndefined = value === undefined;

这部分代码:

  • 使用迭代器函数转换目标值
  • 检查转换后的值是什么类型:
    • valIsNaN:是否为 NaN(利用 NaN 不等于自身的特性)
    • valIsNull:是否为 null
    • valIsSymbol:是否为 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;
  }
}

这是标准的二分查找循环,让我们详细解释每个变量的含义:

  • lowhigh:当前搜索区间的下界和上界索引
  • mid:搜索区间的中间索引,使用 nativeFloor(即 Math.floor)向下取整
  • computed:中间位置元素经过迭代器函数转换后的值
  • othIsDefined:检查 computed 是否不是 undefined,如果是则为 true
  • othIsNull:检查 computed 是否为 null,如果是则为 true
  • othIsReflexive:检查 computed 是否等于自身,这是检测 NaN 的技巧(NaN !== NaN)
  • othIsSymbol:检查 computed 是否为 Symbol 类型,使用 isSymbol 函数判断
  • setLow:决定是调整下界还是上界的标志变量,在复杂比较逻辑中设置

这些变量用于处理不同类型的边界情况:

  1. othIsReflexive 专门用来检测 NaN:如果 computed 是 NaN,则 othIsReflexivefalse
  2. othIsDefinedothIsNull 用于处理 undefinednull 这些特殊值
  3. othIsSymbol 用于处理 Symbol 类型,因为 Symbol 值不能与其他类型直接比较

在每次循环迭代中:

  • 如果 setLowtrue,说明需要在右半部分继续查找,所以将 low 设置为 mid + 1
  • 如果 setLowfalse,说明需要在左半部分继续查找,所以将 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;
}

这是函数最复杂的部分,处理各种特殊值的比较。让我们详细解析每个条件分支处理的具体场景:

  1. 目标值为 NaN 的情况

    javascript 复制代码
    if (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 后面)
  2. 目标值为 undefined 的情况

    javascript 复制代码
    else if (valIsUndefined) {
      setLow = othIsReflexive && (retHighest || othIsDefined);
    }
    • 场景:当我们要查找 undefined 应该插入的位置
    • 处理策略:
      • 首先,中间元素不能是 NaN(othIsReflexive 必须为 true)
      • 其次,如果 retHighest 为 true 或中间元素不是 undefined(othIsDefined 为 true),则 setLow 为 true
      • 这意味着 undefined 会被放在所有 NaN 之前,以及所有非 undefined 值之后
    • 举例:在 [1, undefined, 3] 中寻找 undefined 的插入位置
      • retHighest 为 false,结果为索引 1
      • retHighest 为 true,结果为索引 2(最后一个 undefined 后面)
  3. 目标值为 null 的情况

    javascript 复制代码
    else 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 值之后
    • 举例:在 [1, null, 3] 中寻找 null 的插入位置
      • retHighest 为 false,结果为索引 1
      • retHighest 为 true,结果为索引 2(最后一个 null 后面)
  4. 目标值为 Symbol 的情况

    javascript 复制代码
    else 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 值之后
    • 举例:在 [1, Symbol(), 3] 中寻找 Symbol 的插入位置
      • retHighest 为 false,结果为索引 1
      • retHighest 为 true,结果为索引 2(最后一个 Symbol 后面)
  5. 中间元素为特殊值的情况

    javascript 复制代码
    else if (othIsNull || othIsSymbol) {
      setLow = false;
    }
    • 场景:当目标值是普通值,但中间元素是 null 或 Symbol
    • 处理策略:
      • 直接设置 setLow 为 false,表示在左半部分继续查找
      • 这意味着普通值会被放在所有 null 和 Symbol 之前
    • 举例:在 [null, Symbol(), 3] 中寻找 2 的插入位置
      • 结果为索引 0,因为普通数值应该排在 null 和 Symbol 之前
  6. 普通值的情况

    javascript 复制代码
    else {
      setLow = retHighest ? computed <= value : computed < value;
    }
    • 场景:当目标值和中间元素都是普通值
    • 处理策略:
      • retHighest 为 false 时,使用 < 比较,查找第一个不小于目标值的位置
      • retHighest 为 true 时,使用 <= 比较,查找最后一个不大于目标值的位置
    • 举例:在升序数组 [1, 2, 2, 3] 中寻找 2 的插入位置
      • retHighest 为 false,结果为索引 1(第一个 2)
      • retHighest 为 true,结果为索引 3(最后一个 2 后面)

这种复杂的比较逻辑确保了各种 JavaScript 值类型都有一个确定的排序顺序:

  1. 普通值(按自然升序)
  2. Symbol 值
  3. null 值
  4. undefined 值
  5. NaN 值

并且,对于相等的值,可以根据 retHighest 参数决定是返回第一个位置还是最后一个位置,这种灵活性使得函数可以支持多种查找需求。例如,

  • retHighestfalse时,函数查找第一个不小于目标值的位置(用于sortedIndexBy
  • retHighesttrue时,函数查找最后一个不大于目标值的位置(用于sortedLastIndexBy

简单来说就是

  1. NaN 值的处理
    • 当目标值是 NaN 时,根据retHighest和中间元素是否为 NaN 决定搜索方向
  2. undefined 值的处理
    • 当目标值是 undefined 时,如果中间元素不是 NaN,且满足特定条件,则在右侧搜索
  3. null 值的处理
    • 当目标值是 null 时,如果中间元素既不是 NaN 也不是 undefined,且满足特定条件,则在右侧搜索
  4. Symbol 值的处理
    • 当目标值是 Symbol 时,如果中间元素不是 NaN、undefined 或 null,且满足特定条件,则在右侧搜索
  5. 中间元素为特殊值
    • 如果中间元素是 null 或 Symbol,则在左侧搜索
  6. 普通值的比较
    • 对于普通值,使用<<=比较,取决于retHighest参数

返回结果

javascript 复制代码
return nativeMin(high, MAX_ARRAY_INDEX);

最后返回找到的位置,使用nativeMin确保不超过最大数组索引(MAX_ARRAY_INDEX),这是一个安全措施。

总结

baseSortedIndexBy是 Lodash 中一个设计精巧的内部函数,通过几点我们可以学习到很多:

  1. 算法优化

    • 使用二分查找将时间复杂度从 O(n)降低到 O(log n)
    • 短路处理空数组,避免不必要的计算
  2. 健壮性设计

    • 全面处理 JavaScript 中的特殊值比较
    • 安全地处理各种边界情况
    • 防止返回值超出有效范围
  3. API 灵活性

    • 支持自定义转换函数,适应不同需求
    • 通过参数控制返回相等元素的最低或最高索引
    • 为多个公开 API 提供统一实现,减少代码重复
  4. 代码可读性

    • 清晰的变量命名(虽然逻辑复杂)
    • 将比较逻辑按不同类型分类处理
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax