es-toolkit/iterator 的 uniqBy:用派生键对迭代器做流式惰性去重

发布时间:2026/9/16 15:00:42
es-toolkit/iterator 的 uniqBy:用派生键对迭代器做流式惰性去重 es-toolkit/iterator 的 uniqBy用派生键对迭代器做流式惰性去重【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkituniqBy是 es-toolkit 的es-toolkit/iterator模块提供的惰性迭代器工具它按getKey映射出的键对流经的每个元素去重保留首次出现的顺序并且采用与Set一致的 SameValueZero 比较语义。读完本文你将掌握uniqBy的基本用法、流式streaming去重的原理、与pipe组合的柯里化形式以及它与数组版uniqBy的适用场景取舍并能在事件流、日志、传感器数据等场景中直接落地。uniqBy 是什么不是数组工具而是迭代器管道的一员es-toolkit/iterator的设计理念是不每一步都构建中间数组而是让一条迭代器管道一次只处理一个元素、只做被消费的那部分工作。官方文档在 docs/iterator/intro.md 中明确指出该模块只补齐原生迭代器助手缺失的能力——基于计数的take、drop、map、filter等已存在于Iterator.prototype上es-toolkit 补充的是基于谓词、带状态、以及多数据源的操作uniqBy就是其中典型的带状态操作它内部维护一个已见键的集合从而实现对流的去重。const unique uniqBy(source, getKey);与数组版 src/array/uniqBy.ts先把整个数组处理完再返回新数组不同迭代器版的 src/iterator/uniqBy.ts 返回一个惰性的IteratorObject元素在被消费时才真正从source中拉取。基本用法按派生键去重并保留首次出现顺序uniqBy(source, getKey)接收两个参数sourceIteratorT要去重的迭代器来自array.values()、生成器函数、Map/Set的迭代器等getKey(value: T) K把每个元素映射为用于判定重复的键。返回IteratorObjectT, undefined一个惰性迭代器去掉了重复键对应的元素且携带全部原生迭代器助手map、take、toArray等可以继续无缝链式调用。最简单的例子——保留每个映射键对应的第一个元素import { uniqBy } from es-toolkit/iterator; // 保留每个映射键对应的第一个元素。 uniqBy([1.1, 1.2, 2.3, 2.4].values(), Math.floor).toArray(); // 返回: [1.1, 2.3]按派生键对对象去重——例如每个用户只保留第一个事件import { uniqBy } from es-toolkit/iterator; const events [ { userId: 1, type: click }, { userId: 1, type: view }, { userId: 2, type: click }, ]; uniqBy(events.values(), e e.userId).toArray(); // 返回: [{ userId: 1, type: click }, { userId: 2, type: click }]键的比较语义SameValueZeroNaN 也能去重文档中明确强调uniqBy的键比较采用SameValueZero 语义与Set一致因此NaN键可以被正确去重。这一点在源码 src/iterator/uniqBy.ts 中体现为直接用Set作为已见集合export function uniqByT, K(source: IteratorT, getKey: (value: T) K): IteratorObjectT, undefined { const seen new SetK(); return iterator( function () { let result source.next(); while (!result.done) { const key getKey(result.value); if (!seen.has(key)) { seen.add(key); return { value: result.value, done: false }; } result source.next(); } return { value: undefined, done: true }; }, () void source.return?.() ); }测试 src/iterator/uniqBy.spec.ts 专门验证了这一点it(uses SameValueZero so NaN keys dedupe, () { expect(uniqBy([NaN, NaN, 1].values(), x x).toArray()).toEqual([NaN, 1]); });对比Object.isNaN之间相等与严格相等NaN ! NaNSameValueZero 的取舍在于它视NaN与自身相等从而能去重同时又把0与-0视为相等。选择Set作为底层结构既天然具备这个语义也让查找与插入都保持常数级复杂度键可以是任意值类型数字、字符串、对象引用等。流式去重与无限迭代器配合的关键uniqBy的去重是流式streaming的每个元素一旦被判定为唯一就立刻产出而不会预先消费整个数据源。这意味着它可以配合短路助手如原生take作用于无限迭代器。官方文档指出Deduplication is streaming: each element is emitted as soon as it is found to be unique, so it works with infinite iterators when bounded by a short-circuiting helper.测试用例 src/iterator/uniqBy.spec.ts 用无限迭代器直接验证了这一点it(streams: emits a unique element before consuming the rest, () { let n 0; const infinite: Iteratornumber { next: () ({ value: n, done: false }) }; expect( uniqBy(infinite, x x % 3) .take(3) .toArray() ).toEqual([0, 1, 2]); });这里的infinite永远不会done但uniqBy每次只拉取一个元素、判断键x % 3是否出现过配合.take(3)在产出 3 个唯一元素后立即短路。若换成分批处理的数组式实现这样的输入会直接挂死或内存溢出。一个实际的场景组合用 iterate 生成序列再按周期键去重或者把每个用户 ID 的第一个事件应用在实时事件流上——只要数据源本身是迭代器或生成器uniqBy就能边拉边判。单次消费与资源释放IteratorClose 协议与所有 JavaScript 迭代器一样uniqBy的返回结果是**单次消费single-shot**的一旦被消费完后续next()只会返回done。测试确认了这一点it(is single-shot once consumed, () { const it uniqBy([1, 1, 2].values(), x x); expect(it.toArray()).toEqual([1, 2]); expect(it.toArray()).toEqual([]); });更重要的行为在提前终止时的资源释放上。注意源码中的第二个回调() void source.return?.()。它注册为内部iterator助手的onClose回调当消费者提前结束如take达到上限、for...of中break、getKey抛异常、或迭代器自然结束时都会关闭上游source让生成器里的try/finally清理逻辑可靠执行。该协议实现在 src/iterator/_internal/iterator.tsonClose保证恰好执行一次消费者提前return()、next抛错、或next报告 done三者先到先触发关闭后next不再被调用后续步骤恒为 done。测试对两条路径分别做了验证——提前停止和回调抛错都要关闭上游it(closes the source when the consumer stops early, () { const { source, isClosed } closableSource([1, 2, 3, 4]); uniqBy(source, x x) .take(1) .toArray(); expect(isClosed()).toBe(true); }); it(closes the source when getKey throws, () { const { source, isClosed } closableSource([1, 2, 3]); expect(() uniqBy(source, () { throw new Error(boom); }).toArray() ).toThrow(boom); expect(isClosed()).toBe(true); });这两条测试共同保证了无论正常结束、消费者提前离开还是getKey抛错上游资源都不会泄漏——这正是 docs/iterator/intro.md 中try/finallycleanup in generator sources runs reliably的落地。与 pipe 组合柯里化形式当使用pipe组合变换时从es-toolkit/fp/iterator导入柯里化版本它只接收键函数返回一个接收迭代器的函数。对应实现是 src/fp/iterator/uniqBy.ts本质是封装了一层参数翻转export function uniqByT, K(getKey: (value: T) K): (source: IteratorT) IteratorObjectT, undefined { return function uniqByInIterator(source: IteratorT): IteratorObjectT, undefined { return uniqByIterator(source, getKey); }; }用法示例import { pipe } from es-toolkit/fp; import { toArray, uniqBy } from es-toolkit/fp/iterator; pipe([1.1, 1.2, 2.3, 2.4].values(), uniqBy(Math.floor), toArray()); // 返回: [1.1, 2.3]柯里化形式让uniqBy可以像filter、map、take一样平滑地嵌入管道配合管道中同样来自es-toolkit/fp/iterator的原生助手包装map、filter、take、toArray等形成一条完整的数据变换链。注意导入路径的差异非柯里化版从es-toolkit/iterator导入柯里化版从es-toolkit/fp/iterator导入两者通过 src/fp/iterator/index.ts 统一导出。数组版 vs 迭代器版何时用哪个es-toolkit 同时提供数组版uniqBysrc/array/uniqBy.ts用Map保留首个元素后Array.from(map.values())返回新数组。选择建议依据 docs/iterator/intro.md 的官方定位数据已经是数组且会被完整处理——默认用数组版uniqBy输入很大或无限、管道可能提前结束、或数据本来就是迭代器/生成器——用es-toolkit/iterator的uniqBy。一个典型的对比场景对一条不断产生事件的流做每个用户保留首个事件数组版必须等流结束才能去重且占用与流等量的内存迭代器版则边接收边产出第一个唯一事件配合take甚至可以在拿到所需结果后立刻关闭上游、停止消费。小结uniqByforIterators是 es-toolkit 迭代器工具集中一个简单但设计完整的操作按派生键去重getKey把元素映射为键首次出现的元素被保留SameValueZero 语义底层Set保证NaN键正确去重见 src/iterator/uniqBy.ts流式惰性每个唯一元素立即产出配合原生take等短路助手可安全处理无限迭代器健壮的资源管理单次消费、提前终止或getKey抛错时都会通过source.return()关闭上游遵循 IteratorClose 协议src/iterator/_internal/iterator.ts管道友好es-toolkit/fp/iterator提供柯里化版本可直接嵌入pipe。相关参考uniqBy 文档、迭代器模块总览、柯里化实现、单元测试。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考