es-toolkit 数组去重函数 uniq 全解析:基于 Set 的 O(n) 实现与边界行为
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
uniq 是 es-toolkit 提供的高性能数组去重函数,它基于原生Set的 SameValueZero 相等语义,在保持首次出现顺序的同时以 O(n) 时间复杂度完成去重。本文以官方参考文档 docs/ja/reference/array/uniq.md(英文版见 docs/reference/array/uniq.md)为主体,结合源码与测试用例,深入讲解 uniq 的用法、实现原理、边界行为及其与 uniqBy、uniqWith 的选型关系。
uniq 是什么
uniq 接收一个数组,返回一个去除重复元素后的新数组。它与 lodash 的_.uniq功能一致,但 es-toolkit 的现代实现更轻量、更快速,是 lodash 的高性能替代方案。
const uniqueArray = uniq(arr);核心特性:
- 去重:返回的新数组中每个元素只出现一次;
- 保序:保留元素在原始数组中首次出现的顺序;
- 不修改原数组:返回全新的数组,输入数组保持不变;
- O(n) 时间复杂度:得益于内部使用
Set,整体复杂度为线性。
使用方法
uniq适用于"从数组中移除重复值、只保留唯一值"的场景,它会保留原始数组中元素首次出现的顺序。
import { uniq } from 'es-toolkit/array'; // 从数字数组中去除重复项 const numbers = [1, 2, 2, 3, 4, 4, 5]; const uniqueNumbers = uniq(numbers); console.log(uniqueNumbers); // [1, 2, 3, 4, 5] // 从字符串数组中去除重复项 const words = ['apple', 'banana', 'apple', 'cherry', 'banana']; const uniqueWords = uniq(words); console.log(uniqueWords); // ['apple', 'banana', 'cherry'] // 去除对象数组中引用相同的对象 const obj1 = { id: 1 }; const obj2 = { id: 2 }; const obj3 = { id: 3 }; const objects = [obj1, obj2, obj1, obj3, obj2]; const uniqueObjects = uniq(objects); console.log(uniqueObjects); // [{ id: 1 }, { id: 2 }, { id: 3 }]注意对象数组的去重基于引用相等(SameValueZero),obj1与另一个{ id: 1 }字面量即使结构相同也会被视为不同元素。如果希望按内容或字段去重,应改用 uniqBy 或 uniqWith(详见下文)。
空数组输入会返回空数组:
import { uniq } from 'es-toolkit/array'; const emptyArray = uniq([]); console.log(emptyArray); // []参数与返回值
| 项目 | 说明 |
|---|---|
参数arr | readonly T[]:要去重的数组 |
| 返回值 | T[]:去除重复后的新数组,保留原始数组中首次出现的顺序 |
由于参数类型为readonly T[],传入as const断言或ReadonlyArray类型的数组同样可以直接使用,不会出现类型错误。
源码实现:为什么这么快
uniq 的完整实现位于 src/array/uniq.ts,全函数只有一行:
export function uniq<T>(arr: readonly T[]): T[] { return [...new Set(arr)]; }这行代码同时完成了三件事:
new Set(arr):利用Set的 SameValueZero 语义自动去重。Set内部通常基于哈希表实现,插入与查找平均为 O(1),因此整体去重复杂度为 O(n)——这是与 lodash 经典双循环或排序实现相比的主要性能优势;- 展开运算符
[...]:将Set迭代回普通数组,且Set的迭代顺序就是元素首次插入的顺序,天然保持了原始数组的次序; - 返回新数组:与输入数组是不同引用,不会修改原数组。
相等语义:SameValueZero
uniq 去重时遵循与Set、Map、Array.prototype.includes一致的SameValueZero相等语义,这一点与使用===的朴素去重实现存在明显差异:
NaN与NaN被视为相等(===会认为它们不相等);0与-0被视为相等(===同样认为它们不相等);- 字符串、数字按值比较,对象按引用比较。
测试 src/array/uniq.spec.ts 中的"special values"用例直接验证了这一行为:
const arr = [NaN, NaN, 0, -0, Infinity, -Infinity]; expect(uniq(arr)).toEqual([NaN, 0, Infinity, -Infinity]);即两个NaN只保留一个、0与-0只保留一个。
边界行为:测试用例验证
src/array/uniq.spec.ts 对 uniq 的各种边界情况做了系统验证,这些行为是使用时的关键事实依据:
| 场景 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 数字去重 | [11, 2, 3, 44, 11, 2, 3] | [11, 2, 3, 44] | 基础去重 |
| 字符串去重 | ['a', 'b', 'b', 'c', 'a'] | ['a', 'b', 'c'] | 基础去重 |
| 布尔去重 | [true, false, true, false, false] | [true, false] | 布尔值参与比较 |
| 空值去重 | [null, undefined, null, undefined] | [null, undefined] | null与undefined彼此不同 |
| 空数组 | [] | [] | 返回空数组 |
| 混合类型 | [1, 'a', 2, 'b', 1, 'a'] | [1, 'a', 2, 'b'] | 不同类型互不相等 |
| 顺序保持 | [1, 2, 2, 3, 4, 4, 5] | [1, 2, 3, 4, 5] | 保留首次出现顺序 |
| 稀疏数组 | [1, , 2, undefined, 3, , 2](含空洞) | [1, undefined, 2, 3] | 空洞被折叠,undefined正常保留 |
| 特殊值 | [NaN, NaN, 0, -0, Infinity, -Infinity] | [NaN, 0, Infinity, -Infinity] | SameValueZero 语义 |
此外,测试还明确断言了两个重要的不可变承诺:
// 返回新数组,而非原数组引用 expect(result).not.toBe(array); // 不修改原数组 const array = [1, 2, 3, 2, 1, 3]; uniq(array); expect(array).toEqual([1, 2, 3, 2, 1, 3]);对稀疏数组(sparse array)的处理值得留意:展开运算符对空洞迭代时得到undefined,因此[1, , 2]这类含空洞的数组在去重时空洞会被折叠为undefined(与已有的undefined去重合并)。若你的数据依赖稀疏数组的语义,请留意这一差异。
相关函数:uniqBy 与 uniqWith 的选型
uniq 处理的是"元素自身相等"的去重。当判定标准不再是元素本身时,es-toolkit 还提供了两个变体,三者均从 src/array/index.ts 统一导出:
uniqBy:按变换结果去重
当需要"把每个元素映射为一个比较键,再按该键去重"时使用 uniqBy,只保留映射结果相同元素中的第一个:
import { uniqBy } from 'es-toolkit/array'; // 按年龄去重 const users = [ { id: 1, name: 'john', age: 30 }, { id: 2, name: 'jane', age: 30 }, { id: 3, name: 'joe', age: 25 }, { id: 4, name: 'jenny', age: 25 }, ]; const uniqueByAge = uniqBy(users, user => user.age); // [{ id: 1, name: 'john', age: 30 }, { id: 3, name: 'joe', age: 25 }]其参数签名为uniqBy(arr, mapper),其中mapper为(item: T, index: number, array: readonly T[]) => U,即除元素本身外还可使用索引与整个数组。
uniqWith:按自定义比较函数去重
当"两个元素是否相等"需要自定义判断逻辑(例如差值小于阈值、忽略大小写、多字段联合比较)时使用 uniqWith:
import { uniqWith } from 'es-toolkit/array'; // 数字差小于 1 视为相同 const numbers = [1.2, 1.5, 2.1, 3.2, 5.7, 5.3, 7.19]; const result = uniqWith(numbers, (a, b) => Math.abs(a - b) < 1); console.log(result); // [1.2, 3.2, 5.7, 7.19] // 忽略大小写比较字符串 const words = ['Apple', 'APPLE', 'banana', 'Banana', 'cherry']; const uniqueCaseInsensitive = uniqWith(words, (a, b) => a.toLowerCase() === b.toLowerCase()); console.log(uniqueCaseInsensitive); // ['Apple', 'banana', 'cherry']其比较函数签名为(item1: T, item2: T) => boolean,两个元素相等时返回true。注意uniqWith需要逐一与已保留元素比较,最坏情况下复杂度为 O(n²),元素规模较大时应优先评估能否用uniqBy转化为 O(n) 的键去重。
选型建议
- 元素按值/引用去重,且无需自定义逻辑 →
uniq; - 需要按某个字段或映射结果去重 →
uniqBy(保持 O(n)); - 需要完全自定义的相等判断(如模糊匹配、忽略大小写)→
uniqWith(注意 O(n²) 最坏情况)。
扩展用法:compat 与 fp 变体
compat 版本:兼容 lodash 入参习惯
src/compat/array/uniq.ts 提供了兼容层,接受ArrayLike<T> | null | undefined作为输入,内部先经isArrayLike校验、Array.from归一化后再调用核心实现;对null、undefined或非类数组对象返回[],与 lodash 的行为对齐,方便迁移既有代码:
uniq(null); // [] uniq(undefined); // [] uniq('hello'); // 字符串按类数组处理fp 版本:惰性求值
src/fp/array/uniq.ts 提供了函数式变体,与pipe配合使用:
import { pipe, uniq } from 'es-toolkit/fp'; pipe([1, 2, 2, 3, 3, 3], uniq()); // => [1, 2, 3]从源码结构看,fp 版通过combineEagerAndLazyFunctions组合了贪心与惰性两条路径:在pipe管道中,uniq 会以惰性Sink形式工作,内部维护一个Set,仅当遇到新值时向下游发射,从而支持流式处理,避免中间结果整体物化带来的额外开销。
总结
uniq 是 es-toolkit 中一个"小而精"的数组工具:一行[...new Set(arr)]即获得 O(n) 去重、SameValueZero 相等语义与首次出现顺序保持三大保证,且测试覆盖了空数组、混合类型、稀疏数组、NaN 与 ±0 等全部关键边界。配合 uniqBy(按键去重)与 uniqWith(自定义比较)两个变体,可以覆盖绝大多数"去重"诉求;若需迁移 lodash 代码或使用函数式管道,仓库还提供了 compat 与 fp 两种形态。相关实现与测试可分别参阅 src/array/uniq.ts、src/array/uniq.spec.ts,以及 src/compat/array/uniq.ts、src/fp/array/uniq.ts。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考