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. 代码可读性

    • 清晰的变量命名(虽然逻辑复杂)
    • 将比较逻辑按不同类型分类处理
相关推荐
独立开阀者_FwtCoder1 小时前
CSS view():JavaScript 滚动动画的终结
前端·javascript·vue.js
咖啡教室1 小时前
用markdown语法制作一个好看的网址导航页面(markdown-web-nav)
前端·javascript·markdown
独立开阀者_FwtCoder1 小时前
Vue 团队“王炸”新作!又一打包工具发布!
前端·javascript·vue.js
天天扭码1 小时前
一分钟解决“3.无重复字符的最长字串问题”(最优解)
前端·javascript·算法
独立开阀者_FwtCoder1 小时前
Promise 引入全新 API!效率提升 300%!
前端·javascript·后端
陈明勇1 小时前
三句话搞定周末出行攻略!我用 AI 生成一日游可视化页面,还能秒上线!
前端·人工智能·mcp
晓得迷路了1 小时前
从 0 到 1:开启 Chrome 插件开发的奇妙之旅
javascript·css·chrome
_一条咸鱼_1 小时前
Vue 样式深入剖析:从基础到源码级理解(十)
前端·javascript·面试
懒羊羊我小弟2 小时前
Vue与React组件化设计对比
前端·vue.js·react.js
_朱志强2 小时前
解决前端vue项目在linux上,npm install,node-sass 安装失败的问题
linux·前端·vue.js