概述
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 各种边界情况的严谨态度。