
es-toolkit 的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本文是 es-toolkit 迭代器iterator模块参考文档的深度解读围绕docs/ja/iterator/reference/uniqBy.md展开。你将掌握如何在流式数据场景中按派生键去除重复元素例如每个用户 ID 只保留第一条事件理解其 SameValueZero 键比较语义、流式输出的实现原理、对无限迭代器的支持方式以及如何通过pipe以函数式风格组合使用。阅读后可立即在真实项目中按需引入es-toolkit/iterator或es-toolkit/fp/iterator。一、uniqBy是什么面向迭代器的延迟去重uniqBy是 es-toolkit 迭代器模块提供的函数用于延迟地lazily生成输入迭代器中映射键尚未出现过的元素。与常见的一次性数组去重不同它面向的是Iterator数据流逐个元素判断、逐个元素产出。const unique uniqBy(source, getKey);其核心签名如下见 src/iterator/uniqBy.tsexport function uniqByT, K(source: IteratorT, getKey: (value: T) K): IteratorObjectT, undefined;函数接受两个参数sourceIteratorT需要进行去重的迭代器getKey(value: T) K将每个元素转换为用于检测重复的键的函数。返回一个IteratorObjectT, undefined即移除重复键元素后的延迟求值迭代器。该返回值继承原生Iterator.prototype自带map、filter、take、drop、flatMap、reduce、toArray等全部原生迭代器辅助方法可以直接继续链式调用。在 es-toolkit 的迭代器模块中uniqBy与cartesianProduct、chunk、count、dropWhile、head、iterate、partition、range、scan、takeWhile、zip一同从src/iterator/index.ts导出。二、核心用法按派生键去重场景一对数值流按向下取整结果去重import { uniqBy } from es-toolkit/iterator; // 每个映射键只保留第一个元素。 uniqBy([1.1, 1.2, 2.3, 2.4].values(), Math.floor).toArray(); // 结果: [1.1, 2.3]这里getKey是Math.floor1.1与1.2都映射为键1因此只保留先出现的1.12.3与2.4都映射为键2只保留先出现的2.3。场景二按派生键对对象去重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 }]这是原文档中给出的经典应用保留每个用户 ID 的第一条事件。getKey返回e.userId因此userId: 1的两条记录被去重为一条userId: 2的记录保留。行为特征从原文档与源码实现可以总结出uniqBy的三个关键行为保持首次出现顺序去重后元素的相对顺序与源迭代器中的首次出现顺序一致SameValueZero 键比较语义与Set的判定一致因此NaN作为键也能被正确去重NaN NaN为false但 SameValueZero 认为它们相等这一点在测试 src/iterator/uniqBy.spec.ts 中有明确验证流式去重每个元素一旦被判定为唯一就立即输出因此配合短路short-circuit辅助函数可以作用于无限迭代器。三、源码级原理Set记忆 逐元素拉取核心实现uniqBy的完整实现非常简洁位于 src/iterator/uniqBy.tsexport 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?.() ); }从源码可以看出其工作原理用SetK作为已见键的记忆结构这正是 SameValueZero 语义的来源每次调用next时从source拉取元素、计算键、查询Set键未见过则记录并立即产出该元素键已见过则继续拉取下一个元素当源迭代器耗尽done: true时返回终止结果第二个回调参数onClose用于在迭代器关闭时调用source.return?.()从而把关闭信号传播给上游资源。底层iterator辅助函数uniqBy返回的迭代器由 _internal/iterator.ts 中的iterator辅助函数构建它实现了完整的IteratorClose协议返回对象的原型是原生Iterator.prototype因此行为与内建迭代器辅助方法如array.values().map(...)完全一致一次性single-shot消费、可通过Symbol.iterator迭代返回自身、携带全部原生辅助方法onClose在三种情况下恰好执行一次消费者提前终止return()例如take或for...of中的break、next抛出异常、next报告done迭代器关闭后next不再被调用后续每一步都直接返回done。值得一提的设计细节这里刻意用手写的next函数而非生成器generator实现注释说明直接驱动迭代器协议相比yield生成器大约快两倍而Object.create(Iterator.prototype)相比普通对象字面量没有可测量开销。这是 es-toolkit 追求性能的体现。测试验证的边界行为src/iterator/uniqBy.spec.ts 的测试用例完整覆盖了上述行为每个映射键保留第一个元素L17-L19NaN键按 SameValueZero 去重uniqBy([NaN, NaN, 1].values(), x x).toArray()得到[NaN, 1]L21-L23按派生键对对象去重L25-L35流式对无限迭代器uniqBy(infinite, x x % 3).take(3).toArray()得到[0, 1, 2]即只消费到产出 3 个唯一元素为止L37-L46一次性toArray()消费完成后再次调用返回空数组L48-L52提前关闭用take(1)提前终止时上游生成器源的finally块被执行isClosed()为trueL54-L62异常传播与关闭getKey抛出异常时异常向上抛出且上游源被正确关闭L64-L73。四、流式去重与无限迭代器原文档强调去重是流式进行的每个元素一旦确认唯一就立即输出因此只要用短路辅助函数限定范围uniqBy就可以用于无限迭代器。测试中的示例直观展示了这一能力let n 0; const infinite: Iteratornumber { next: () ({ value: n, done: false }) }; uniqBy(infinite, x x % 3) .take(3) .toArray(); // 结果: [0, 1, 2]其流程为uniqBy逐个计算键0、1、2 各出现一次均为唯一并输出当take(3)集齐 3 个元素后触发return()此时uniqBy的onClose回调调用source.return?.()关闭上游整个管道停止——无限迭代器永远不会被完整消费。五、与pipe组合函数式风格柯里化形式当需要通过pipe组合变换时应从es-toolkit/fp/iterator导入柯里化形式它只接收键函数返回一个接收迭代器的函数。对应实现见 src/fp/iterator/uniqBy.tsexport function uniqByT, K(getKey: (value: T) K): (source: IteratorT) IteratorObjectT, undefined { return function uniqByInIterator(source: IteratorT): IteratorObjectT, undefined { return uniqByIterator(source, getKey); }; }即uniqBy(getKey)返回(source: IteratorT) IteratorObjectT, undefined内部委托给上一节讲到的核心实现。pipe 用法示例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(Math.floor)作为数据在后的操作符接入pipe数据从左到右依次流过各函数最终由toArray()收尾。关于pipe的机制见 src/fp/pipe.ts它会把连续的惰性函数map、filter、take等分组融合成一次逐元素的短路遍历避免每一步都构建中间数组当末尾存在take(n)这类短路函数时凑齐n个结果即停止剩余输入不再被访问。这也解释了为何uniqBy这类惰性操作符在pipe中特别有用——整个管道保持单遍、可早停。uniqBy在es-toolkit/fp/iterator中与cartesianProduct、chunk、count、drop、dropWhile、every、filter、find、flatMap、forEach、head、map、partition、reduce、scan、some、take、takeWhile、toArray、zip一同导出见 src/fp/iterator/index.ts。六、总结何时使用uniqBy需求推荐方式按某个派生键对流式数据去重保持首次出现顺序uniqBy(source, getKey)es-toolkit/iterator对无限迭代器去重只取前 N 个唯一结果uniqBy(source, getKey).take(n)在函数式管道中组合去重与其他变换pipe(source, uniqBy(getKey), toArray())es-toolkit/fp/iterator需要处理NaN等特殊键的去重uniqBy使用 SameValueZero 语义天然支持作为现代 lodash 的升级替代方案es-toolkit 以更小的体积和更快的速度为目标而uniqBy的迭代器实现通过Set记忆、逐元素拉取和完整的 IteratorClose 协议在保持语义正确的同时兼顾了流式处理与资源释放。如需深入了解迭代器模块的其他函数可继续查阅 迭代器模块文档 与对应的 英文参考。【免费下载链接】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),仅供参考