es-toolkit 数组去重函数 uniq 全解析:基于 Set 的 O(n) 实现与边界行为
2026/9/17 4:29:29 网站建设 项目流程

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); // []

参数与返回值

项目说明
参数arrreadonly T[]:要去重的数组
返回值T[]:去除重复后的新数组,保留原始数组中首次出现的顺序

由于参数类型为readonly T[],传入as const断言或ReadonlyArray类型的数组同样可以直接使用,不会出现类型错误。

源码实现:为什么这么快

uniq 的完整实现位于 src/array/uniq.ts,全函数只有一行:

export function uniq<T>(arr: readonly T[]): T[] { return [...new Set(arr)]; }

这行代码同时完成了三件事:

  1. new Set(arr):利用Set的 SameValueZero 语义自动去重。Set内部通常基于哈希表实现,插入与查找平均为 O(1),因此整体去重复杂度为 O(n)——这是与 lodash 经典双循环或排序实现相比的主要性能优势;
  2. 展开运算符[...]:将Set迭代回普通数组,且Set的迭代顺序就是元素首次插入的顺序,天然保持了原始数组的次序;
  3. 返回新数组:与输入数组是不同引用,不会修改原数组。

相等语义:SameValueZero

uniq 去重时遵循与SetMapArray.prototype.includes一致的SameValueZero相等语义,这一点与使用===的朴素去重实现存在明显差异:

  • NaNNaN被视为相等(===会认为它们不相等);
  • 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]nullundefined彼此不同
空数组[][]返回空数组
混合类型[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归一化后再调用核心实现;对nullundefined或非类数组对象返回[],与 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询