Lodash源码阅读-baseUniq

概述

baseUniq 是 Lodash 内部的基础函数,用于数组去重操作,支持自定义迭代器和比较器,是 _.uniq_.uniqBy 等方法的底层实现。

前置学习

  • 依赖函数:

    • arrayIncludes: 检查数组是否包含指定元素
    • arrayIncludesWith: 使用比较器检查数组是否包含指定元素
    • createSet: 创建一个 Set 集合
    • setToArray: 将 Set 转换为数组
    • cacheHas: 检查缓存中是否存在某个值
    • SetCache: 一个用于存储唯一值的缓存类
  • 技术知识:

    • JavaScript 数组操作
    • Set 集合的使用
    • 标签化循环与 continue
    • NaN 值的特殊处理 (NaN !== NaN)

源码实现

javascript 复制代码
function baseUniq(array, iteratee, comparator) {
  var index = -1,
    includes = arrayIncludes,
    length = array.length,
    isCommon = true,
    result = [],
    seen = result;

  if (comparator) {
    isCommon = false;
    includes = arrayIncludesWith;
  } else if (length >= LARGE_ARRAY_SIZE) {
    var set = iteratee ? null : createSet(array);
    if (set) {
      return setToArray(set);
    }
    isCommon = false;
    includes = cacheHas;
    seen = new SetCache();
  } else {
    seen = iteratee ? [] : result;
  }
  outer: while (++index < length) {
    var value = array[index],
      computed = iteratee ? iteratee(value) : value;

    value = comparator || value !== 0 ? value : 0;
    if (isCommon && computed === computed) {
      var seenIndex = seen.length;
      while (seenIndex--) {
        if (seen[seenIndex] === computed) {
          continue outer;
        }
      }
      if (iteratee) {
        seen.push(computed);
      }
      result.push(value);
    } else if (!includes(seen, computed, comparator)) {
      if (seen !== result) {
        seen.push(computed);
      }
      result.push(value);
    }
  }
  return result;
}

实现思路

baseUniq 根据输入参数和数组大小选择最优的去重策略。函数会遍历数组,对每个元素应用可选的迭代函数,然后检查转换后的值是否已经在结果集中出现过。如果有自定义比较器,使用比较器进行比较;如果数组较大,使用缓存或 Set 提高性能;对于一般情况,采用简单数组比较。函数还特别处理了 JavaScript 中的特殊值如 NaN 和 -0/+0。

源码解析

1. 变量初始化与准备工作

javascript 复制代码
var index = -1,
  includes = arrayIncludes,
  length = array.length,
  isCommon = true,
  result = [],
  seen = result;

这部分代码初始化了几个关键变量:

  • index = -1:数组遍历指针,从-1 开始是因为循环中使用前置自增(++index
  • includes = arrayIncludes:默认使用的元素查找函数,可以检查一个值是否在数组中
  • length = array.length:缓存数组长度,避免循环中重复计算
  • isCommon = true:标记是否使用通用模式(简单的遍历比较)
  • result = []:存储去重后的结果数组
  • seen = result:存储已经见过的值,初始时指向结果数组(共享引用可以节省内存)

2. 智能策略选择

javascript 复制代码
if (comparator) {
  // 提供了自定义比较器
  isCommon = false;
  includes = arrayIncludesWith;
} else if (length >= LARGE_ARRAY_SIZE) {
  // 大数组优化
  var set = iteratee ? null : createSet(array);
  if (set) {
    return setToArray(set);
  }
  isCommon = false;
  includes = cacheHas;
  seen = new SetCache();
} else {
  // 小数组 + 可能有迭代器
  seen = iteratee ? [] : result;
}

这段代码根据输入参数智能选择最佳去重策略:

情况 1:有自定义比较器

javascript 复制代码
// 例如:_.uniqWith([1, 1.5, 2], function(a, b) { return Math.floor(a) === Math.floor(b); })
if (comparator) {
  isCommon = false; // 不使用通用模式
  includes = arrayIncludesWith; // 使用支持自定义比较的查找函数
}

情况 2:大数组 + 无迭代器

javascript 复制代码
// 例如处理有1000个元素的数组:_.uniq([1, 2, 3, ..., 1000])
else if (length >= LARGE_ARRAY_SIZE) { // LARGE_ARRAY_SIZE通常是200
  var set = iteratee ? null : createSet(array);
  if (set) {
    // 如果环境支持Set且能创建成功,直接用Set去重(O(n)复杂度)
    return setToArray(set); // 直接返回结果,不再继续执行
  }

  // 如果无法使用Set(有迭代器或环境不支持),使用SetCache
  isCommon = false;
  includes = cacheHas; // 使用缓存查找函数
  seen = new SetCache; // SetCache是Lodash自己实现的类似Set的结构
}

情况 3:小数组 + 可能有迭代器

javascript 复制代码
// 例如:_.uniqBy([{id:1}, {id:2}, {id:1}], 'id')
else {
  // 如果有迭代器,需要单独的数组存储计算后的值
  seen = iteratee ? [] : result;
}

3. 主循环与去重核心逻辑

javascript 复制代码
outer: // 定义循环标签,方便从内层循环跳出
while (++index < length) {
  var value = array[index],
      computed = iteratee ? iteratee(value) : value;

  value = (comparator || value !== 0) ? value : 0;
  // ...

主循环遍历数组的每个元素:

  • value = array[index]:获取当前元素值
  • computed = iteratee ? iteratee(value) : value:如果提供了迭代器,计算转换后的值
    • 例如:_.uniqBy([{id:1}, {id:2}, {id:1}], 'id') 中,computed 会是 1, 2, 1
  • value = (comparator || value !== 0) ? value : 0:特殊处理 +0-0
    • 在 JavaScript 中 +0 === -0true,但 1/+0 !== 1/-0,这里确保它们被视为相同
    • 示例:Object.is(0, -0)false,但 Lodash 需要把它们当作相同处理

4. 通用模式去重(简单数组比较)

javascript 复制代码
if (isCommon && computed === computed) {
  var seenIndex = seen.length;
  while (seenIndex--) {
    if (seen[seenIndex] === computed) {
      continue outer;
    }
  }
  if (iteratee) {
    seen.push(computed);
  }
  result.push(value);
}

这是处理常见情况的代码:小数组 + 无自定义比较器。

  • computed === computed:这个看似奇怪的条件其实是用来排除 NaN,因为 NaN !== NaN

    • 如果 computedNaN,这个条件为 false,会走到 else 分支
    • 示例:对于数组 [1, NaN, 2, NaN],第二个 NaN 会进入不同的处理分支
  • 从后往前遍历 seen 数组,检查 computed 是否已存在:

    javascript 复制代码
    var seenIndex = seen.length;
    while (seenIndex--) { // 从后往前查找通常更高效,因为新添加的元素在末尾
      if (seen[seenIndex] === computed) {
        continue outer; // 如果找到重复,跳过当前元素处理,继续处理下一个元素
      }
    }
  • 如果没有找到重复,将值添加到结果中:

    javascript 复制代码
    if (iteratee) {
      seen.push(computed); // 如果有迭代器,记录计算后的值
    }
    result.push(value); // 把原始值添加到结果数组

示例:

javascript 复制代码
_.uniq([1, 2, 1, 3]); // 最终 seen 和 result 都是 [1, 2, 3]
_.uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], "id"); // seen 是 [1, 2],result 是 [{id:1}, {id:2}]

5. 非通用模式去重(缓存或自定义比较)

javascript 复制代码
else if (!includes(seen, computed, comparator)) {
  if (seen !== result) {
    seen.push(computed);
  }
  result.push(value);
}

这部分处理特殊情况:大数组/自定义比较器/NaN 值

  • !includes(seen, computed, comparator):检查 computed 是否已存在于 seen

    • 如果使用比较器:includes = arrayIncludesWith,会用 comparator 比较
    • 如果使用缓存:includes = cacheHas,会在 SetCache 中查找
    • 示例:_.uniqWith([1.2, 1.8, 2.5], function(a, b) { return Math.floor(a) === Math.floor(b); }) 这里 1.21.8 会被视为相同(因为都向下取整为 1)
  • 如果是新元素(includes 返回 false):

    javascript 复制代码
    if (seen !== result) {
      seen.push(computed); // 如果seen不是result,记录computed值
    }
    result.push(value); // 把原始值添加到结果

6. 处理特殊值示例

NaN 值处理

javascript 复制代码
_.uniq([1, NaN, 2, NaN]); // 结果: [1, NaN, 2]

对于数组中的 NaN,由于 NaN !== NaN,用普通的 === 无法检测到重复的 NaNbaseUniq 通过 computed === computed 检测 NaN,对 NaN 使用特殊路径处理。

-0 和 +0 处理

javascript 复制代码
_.uniq([-0, +0]); // 结果: [0]  (而不是 [-0, +0])

虽然 -0 === +0true,但在其他场景下它们有区别。baseUniq 通过 value = (comparator || value !== 0) ? value : 0 确保它们被视为同一个值。

7. 最终返回

javascript 复制代码
return result; // 返回去重后的数组

至此,baseUniq 完成了数组去重的任务,返回一个包含所有不重复元素的新数组。

总结

baseUniq 是一个高效、灵活的数组去重实现,通过根据不同场景选择最优算法达到性能最优。其特点包括:

  1. 自适应策略选择:根据数组大小和参数选择不同的去重策略
  2. 支持自定义迭代器:可以在比较前对元素进行转换
  3. 支持自定义比较器:灵活定制元素间的相等性判断
  4. 特殊值处理:正确处理 JavaScript 中的特殊值如 NaN 和 +0/-0
  5. 性能优化:对大数组使用 Set 或缓存机制提高性能

这种实现体现了 Lodash 库重视性能和正确性的设计理念,以及处理 JavaScript 各种边界情况的严谨态度。

相关推荐
DN金猿11 分钟前
使用npm install或cnpm install报错解决
前端·npm·node.js
丘山子12 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
志存高远6624 分钟前
Kotlin 的 suspend 关键字
前端
www_pp_37 分钟前
# 构建词汇表:自然语言处理中的关键步骤
前端·javascript·自然语言处理·easyui
YuShiYue1 小时前
pnpm monoreop 打包时 node_modules 内部包 typescript 不能推导出类型报错
javascript·vue.js·typescript·pnpm
天天扭码1 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
一个专注写代码的程序媛1 小时前
为什么vue的key值,不用index?
前端·javascript·vue.js
장숙혜1 小时前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang1 小时前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet
某公司摸鱼前端2 小时前
uniapp 仿企微左边公司切换页
前端·uni-app·企业微信