概述
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 === -0为true,但1/+0 !== 1/-0,这里确保它们被视为相同 - 示例:
Object.is(0, -0)为false,但 Lodash 需要把它们当作相同处理 
- 在 JavaScript 中 
 
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- 如果 
computed是NaN,这个条件为false,会走到else分支 - 示例:对于数组 
[1, NaN, 2, NaN],第二个NaN会进入不同的处理分支 
 - 如果 
 - 
从后往前遍历
seen数组,检查computed是否已存在:javascriptvar seenIndex = seen.length; while (seenIndex--) { // 从后往前查找通常更高效,因为新添加的元素在末尾 if (seen[seenIndex] === computed) { continue outer; // 如果找到重复,跳过当前元素处理,继续处理下一个元素 } } - 
如果没有找到重复,将值添加到结果中:
javascriptif (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.2和1.8会被视为相同(因为都向下取整为 1) 
 - 如果使用比较器:
 - 
如果是新元素(
includes返回false):javascriptif (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,用普通的 === 无法检测到重复的 NaN。 baseUniq 通过 computed === computed 检测 NaN,对 NaN 使用特殊路径处理。
-0 和 +0 处理
            
            
              javascript
              
              
            
          
          _.uniq([-0, +0]); // 结果: [0]  (而不是 [-0, +0])
        虽然 -0 === +0 为 true,但在其他场景下它们有区别。baseUniq 通过 value = (comparator || value !== 0) ? value : 0 确保它们被视为同一个值。
7. 最终返回
            
            
              javascript
              
              
            
          
          return result; // 返回去重后的数组
        至此,baseUniq 完成了数组去重的任务,返回一个包含所有不重复元素的新数组。
总结
baseUniq 是一个高效、灵活的数组去重实现,通过根据不同场景选择最优算法达到性能最优。其特点包括:
- 自适应策略选择:根据数组大小和参数选择不同的去重策略
 - 支持自定义迭代器:可以在比较前对元素进行转换
 - 支持自定义比较器:灵活定制元素间的相等性判断
 - 特殊值处理:正确处理 JavaScript 中的特殊值如 NaN 和 +0/-0
 - 性能优化:对大数组使用 Set 或缓存机制提高性能
 
这种实现体现了 Lodash 库重视性能和正确性的设计理念,以及处理 JavaScript 各种边界情况的严谨态度。