
es-toolkit/fp 的 without在 pipe 流水线中按值剔除数组元素的惰性函数实现【免费下载链接】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导读without是 es-toolkit 函数式编程子模块es-toolkit/fp中的数组操作算子调用without(...values)会返回一个接收数组、返回剔除了指定值的新数组的函数专门用于与pipe组合成数据处理流水线。本文将以 docs/ja/fp/reference/without.md 为骨架结合 src/fp/array/without.ts、src/fp/_internal/lazy.ts 与 src/fp/pipe.ts 的源码讲清楚它的用法、参数语义、与普通版without的差异以及它在pipe内惰性求值lazy evaluation背后的实现原理。读完你将掌握如何用它做按值过滤以及在什么场景下它能与map、take等算子融合为单趟短路执行。一、without是什么在函数式编程风格中我们通常希望把处理逻辑写成一个个可组合的算子operator再用pipe把数据从左到右依次灌入。without就是这个体系里负责按值剔除的算子const result pipe(array, without(...values));它接收待剔除的值列表values返回一个(array: readonly T[]) T[]类型的函数这个返回的函数会从传入的数组中移除所有与values中任一值相等的元素并返回新数组。适用建议如果你只是想在普通代码里一次性过滤数组官方文档推荐直接使用 es-toolkit 的普通版without只有当你在用pipe串联多个转换时才应该使用这里的fp版without在pipe内它支持惰性求值。二、快速上手在pipe中按值过滤最典型的用法如下来自原文档示例见 docs/ja/fp/reference/without.mdimport { pipe, without } from es-toolkit/fp; pipe([1, 2, 3, 2], without(2)); // [1, 3]执行过程可以拆解为两步理解without(2)先把待剔除值收集起来返回一个过滤函数pipe将数组[1, 2, 3, 2]作为输入传给该过滤函数凡是等于2的元素都被剔除得到[1, 3]。由于without接收的是可变参数rest 参数一次可以剔除多个值import { pipe, without } from es-toolkit/fp; pipe([1, 2, 3, 4, 5], without(2, 4)); // [1, 3, 5] pipe([a, b, c, a], without(a)); // [b, c]参数与返回值valuesT[]要从管道输入的数组中剔除的值列表。可以传零个、一个或多个值返回值(array: readonly T[]) T[]一个数据末置data-last的函数——数组作为最后一个参数传入这正是pipe组合所需的形态。原数组不会被修改返回的是剔除指定值之后的全新数组这一点与普通版行为一致普通版文档明确说明原数组不变返回新数组见 docs/reference/array/without.md。三、fp 版与普通版without的关系es-toolkit 在es-toolkit/array下提供的是数据优先的普通版import { without } from es-toolkit/array; without([1, 2, 3, 4, 5], 2, 4); // [1, 3, 5] without([a, b, c, a], a); // [b, c]普通版还正确处理NaNimport { without } from es-toolkit/array; without([1, NaN, 3, NaN, 5], NaN); // [1, 3, 5]fp 版并没有重新实现一套过滤逻辑而是直接复用了普通版在 src/fp/array/without.ts 中withoutEager内部调用了从../../array/without.ts导入的withoutToolkit。换句话说普通版without(array, ...values)参数顺序是数组在前值在后适合直接调用fp 版without(...values)返回吃数组的函数参数顺序是值在前数组在后data-last适合放进pipe两者共享同一套剔除语义fp 版的急切eager路径就是普通版的完整实现。从源码结构看src/array/without.ts普通版without本质上是difference(array, values)的薄封装而differencesrc/array/difference.ts通过new Set(secondArr)构建待剔除集合再用firstArr.filter(item !secondSet.has(item))完成过滤。这意味着待剔除值集合只需构建一次后续每个元素都是 O(1) 的Set.has查找整体时间复杂度为 O(n m)n 为原数组长度m 为待剔除值个数Set.has采用 SameValueZero 语义比较因此NaN也能被正确识别并剔除——这也是普通版文档强调正确处理NaN的底层原因fp 版沿袭了该行为。四、源码实现剖析惰性能力从哪来fp版without的完整实现如下src/fp/array/without.tsimport { without as withoutToolkit } from ../../array/without.ts; import { combineEagerAndLazyFunctions, createLazyFunction } from ../_internal/lazy.ts; export function withoutT(...values: T[]): (array: readonly T[]) T[] { const valueSet new Set(values); function withoutEager(array: readonly T[]): T[] { return withoutToolkit(array, ...values); } const withoutLazy createLazyFunctionT, T((value, _index, emit) { if (!valueSet.has(value)) { emit(value); } }); return combineEagerAndLazyFunctions(withoutEager, withoutLazy); }拆开看有三个关键部分预构建Setconst valueSet new Set(values)在算子创建时就把待剔除值固化下来。注意这里without的调用时机是配置阶段without(2)而数组在执行阶段才到达pipe把数组传入所以 Set 只构建一次两个阶段里反复执行也不会重复扫描值列表。急切路径withoutEager直接委托普通版实现保证without单独调用不放在pipe里时行为与普通版完全一致。惰性路径withoutLazy通过createLazyFunction声明逐元素处理逻辑——每个元素进来时若不在valueSet中就调用emit(value)转发给下一级算子否则直接丢弃。这个逐元素回调就是惰性融合的基础。最后combineEagerAndLazyFunctions(withoutEager, withoutLazy)把两个路径打包返回的函数直接调用时表现为withoutEager同时通过Object.assign挂上lazy元数据见 src/fp/_internal/lazy.tspipe据此识别它是否可以被惰性融合。五、惰性求值如何工作与pipe的融合与短路原文档指出在pipe中支持惰性求值。要理解这句话需要看es-toolkit/fp的惰性基础设施src/fp/_internal/lazy.ts与pipe的分组调度逻辑src/fp/pipe.ts。1. 推送式push管线而非生成器es-toolkit/fp的惰性机制没有采用 generator 的拉取式模型而是把每个惰性函数改写成推送式变换SinkT是下游的接收器返回false表示运行结束不要再推数据LazyTransform把下游 sink 包装成上游 sinkcreateLazyFunction(step)基于逐元素 step 回调构造变换step收到(value, index, emit)可以emit零次丢弃如without剔除元素、一次如map、多次如flatMap返回false则提前终止整条管线。without的惰性 step 正是命中剔除集合就不emitconst withoutLazy createLazyFunctionT, T((value, _index, emit) { if (!valueSet.has(value)) { emit(value); } });2.pipe的分组与融合pipe的实现src/fp/pipe.ts会先把所有算子按是否带lazy元数据切成连续分组连续的一串惰性函数归为一组其余逐个调用。对惰性分组lazyPipesrc/fp/pipe.ts把各算子的变换从后往前组合成一条 sink 链然后对输入数组做单趟驱动不构建任何中间数组——map的结果直接流向without再流向take任何一个算子返回false如take(n)收集满 n 个就短路时驱动循环立即break后续输入元素根本不会进入管线。3. 测试如何验证惰性测试用例src/fp/array/without.spec.ts用 spy 精确验证了这一点import { describe, expect, it, vi } from vitest; import { map } from ./map.ts; import { take } from ./take.ts; import { without } from ./without.ts; import { pipe } from ../pipe.ts; it(supports lazy evaluation with a short-circuiting operator, () { const spy vi.fn((value: number) value); expect(pipe([1, 2, 3, 4], map(spy), without(1), take(2))).toEqual([2, 3]); expect(spy).toHaveBeenCalledTimes(3); });输入是 4 个元素但map的间谍函数只被调用了 3 次因为without(1)把1剔除后take(2)在收到2、3两个结果时已集满管线立即短路4从未进入map。如果without不支持惰性map会被调用 4 次、且会生成中间数组。这正是pipe内惰性求值 提前终止的直观证据。4. 惰性触发的条件需要说明的是惰性融合并非无条件生效。从 src/fp/pipe.ts 可以看到只有满足group.lazy isIterable(output) (group.shortCircuit || !Array.isArray(output))时才会走lazyPipe。这意味着当惰性分组中没有take这类短路算子、且输入是普通数组时pipe仍会按每个函数各自执行一遍的急切方式运行避免纯过滤场景下融合带来的额外开销当存在短路算子或输入本身是非数组可迭代对象Set、生成器等时才启用融合单趟执行。这从源码结构上解释了为什么带take的without组合能获得最大的性能收益——例如pipe(array, without(1), take(2))这类场景既能按值剔除又能在结果足够时立即停止遍历。六、实用要点小结关注点说明引入方式import { pipe, without } from es-toolkit/fp;典型用法pipe([1, 2, 3, 2], without(2)) // [1, 3]参数values: T[]可变参数可传多个待剔除值返回值(array: readonly T[]) T[]data-last 过滤函数是否修改原数组否返回新数组NaN处理正确剔除Set.has的 SameValueZero 语义与普通版关系fp 版急切路径直接委托 src/array/without.ts 的普通版实现惰性支持在pipe内可与map、take等融合为单趟短路执行见 src/fp/_internal/lazy.ts适用场景函数式管道组合若只是单次过滤官方建议直接用 普通版 without如果你正在学习es-toolkit/fp的算子体系without是一个很好的切入点它麻雀虽小却完整展示了data-last 算子 急切/惰性双路径 pipe融合调度这一整套设计。结合 src/fp/array/without.spec.ts、src/fp/pipe.ts 与 src/fp/_internal/lazy.ts 一起阅读可以顺带理解map、filter、take等其他惰性算子共同的运行机制。【免费下载链接】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),仅供参考