es-toolkit 的 Iterator 版 filter:基于原生 Iterator.prototype.filter 的惰性筛选与 pipe 组合指南
【免费下载链接】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
es-toolkit/fp/iterator提供的filter用于创建一个“只保留满足条件的元素”的惰性迭代器转换函数,专为与函数式编程的pipe组合而设计。本文以 docs/ja/iterator/reference/filter.md 为核心,结合 src/fp/iterator/filter.ts 的源码实现与类型重载,讲解它的参数语义、返回值、类型收窄能力、底层委托原理,以及它与数组版filter、原生Iterator.prototype.filter的使用边界,帮助你写出既符合函数式风格又保持类型安全的管道代码。
为什么需要es-toolkit/fp/iterator版filter
现代 JavaScript 已经原生提供了Iterator.prototype.filter,可以直接写出source.filter(predicate)。但在函数式组合的场景中,我们希望把转换步骤作为一等函数传入pipe,让每一步都是“接收迭代器、返回新迭代器”的纯函数,而不是在迭代器实例上链式调用方法。
这正是es-toolkit/fp/iterator的定位。filter返回的不是筛选后的结果,而是一个函数:它接收一个Iterator<T>,返回一个惰性求值的IteratorObject。典型用法如下:
const result = pipe(source, filter(predicate));import { pipe } from 'es-toolkit/fp'; import { filter, toArray } from 'es-toolkit/fp/iterator'; // 只保留偶数 pipe( [1, 2, 3, 4].values(), filter(x => x % 2 === 0), toArray() ); // 结果: [2, 4]管道中的filter与toArray各司其职:filter负责惰性筛选,toArray负责把最终的迭代器物化为数组。如果你只是在普通代码里做一次性筛选,官方文档的建议是直接使用原生Iterator.prototype.filter;只有在用pipe串联多个转换时,才需要切换到es-toolkit/fp/iterator这个变体。
filter(predicate)的完整签名
参数
predicate((value: T, index: number) => unknown):对每个元素及其索引调用。当返回值被判定为真值(truthy)时,该元素会被保留;返回假值(falsy)则被丢弃。
注意predicate的返回值类型是unknown而非boolean,这与原生filter的“truthy/falsy 判定”语义保持一致:任何能被隐式转换为真值的返回值都会保留元素。
返回值
(source: Iterator<T>) => IteratorObject<T, undefined>:一个把Iterator<T>映射为“惰性生成保留元素”的IteratorObject的函数。
关键点在于惰性(lazy):filter(predicate)并不会立刻遍历源迭代器,只有当下游真正开始消费(例如调用toArray()或for...of)时,元素才会被逐个取出并经过predicate判定。这意味着它与map、take等组合时,可以做到“边走边筛、提前终止”,详见后文。
源码实现:一行委托给原生Iterator.prototype.filter
打开 src/fp/iterator/filter.ts 可以看到,实现非常精简:
export function filter<T>( predicate: (value: T, index: number) => unknown ): (source: Iterator<T>) => IteratorObject<T, undefined> { return function filterInIterator(source: Iterator<T>): IteratorObject<T, undefined> { return Iterator.from(source).filter(predicate) as IteratorObject<T, undefined>; }; }其核心只有一行:
return Iterator.from(source).filter(predicate);这里的实现策略可以拆解为两层:
- 标准化源迭代器:
Iterator.from(source)是 ES 标准库方法,它把任意符合迭代器协议的对象统一包装为标准Iterator实例,保证后续可以安全调用.filter()。 - 委托原生筛选:实际的筛选逻辑完全交给原生
Iterator.prototype.filter。也就是说,es-toolkit/fp/iterator的filter并没有重新实现一套筛选算法,而是把“惰性 + 类型判定”的底层行为交由引擎原生能力承担,自身只负责柯里化适配,把predicate与source拆成pipe友好的两段式调用。
这种“薄封装”设计的直接收益是:性能与原生方法对齐,且天然继承原生filter的惰性求值语义。从源码结构看,这也是es-toolkit/fp/iterator系列(index.ts 中导出的filter、map、take、takeWhile等)的统一模式。
类型重载:普通谓词与类型守卫
filter提供了两个重载,对应两种类型行为:
// 重载一:谓词是类型守卫,元素类型被收窄为 S export function filter<T, S extends T>( predicate: (value: T, index: number) => value is S ): (source: Iterator<T>) => IteratorObject<S, undefined>; // 重载二:普通谓词,元素类型保持不变 export function filter<T>( predicate: (value: T, index: number) => unknown ): (source: Iterator<T>) => IteratorObject<T, undefined>;当predicate写成类型守卫形式(value): value is S时,TypeScript 会把产出迭代器的元素类型自动收窄为S。这一点让filter兼具“运行时筛选”和“编译期类型过滤”双重能力,例如:
import { pipe } from 'es-toolkit/fp'; import { filter, toArray } from 'es-toolkit/fp/iterator'; const mixed: Iterator<number | string> = [1, 'a', 2, 'b'].values(); const strings = pipe( mixed, filter((x): x is string => typeof x === 'string'), toArray() ); // strings: string[],后续可以直接调用字符串方法而无需再次断言与数组版filter的对比:同一 API 形态,不同数据源
es-toolkit的 fp 模块同样为数组提供了filter,位于 src/fp/array/filter.ts,签名形态完全一致(接收predicate,返回接收数组的函数),但数据源与产出不同:
| 维度 | es-toolkit/fp/iterator的filter | es-toolkit/fp数组版filter |
|---|---|---|
| 输入 | Iterator<T> | readonly T[] |
| 输出 | 惰性的IteratorObject<T> | 立即求值的T[] |
| 求值时机 | 消费时才执行predicate | 调用时立即执行 |
| 底层实现 | 委托原生Iterator.prototype.filter | 自实现数组筛选(见 src/fp/array/filter.ts) |
数组版对应测试 src/fp/array/filter.spec.ts 验证了它的行为契约:
- 只保留通过谓词的元素(
[1, 2, 3, 4]过滤偶数得到[2, 4]); predicate的第二个参数是索引((_value, index) => index % 2 === 0保留偶数位元素);- 类型守卫可收窄结果类型(
(x): x is string得到string[]); - 没有任何元素匹配时返回空数组。
选择哪个版本,取决于你是在“管道中处理数组”还是“管道中处理迭代器”:前者用es-toolkit/fp的filter,后者用es-toolkit/fp/iterator的filter。
惰性求值与管道融合:一次遍历、提前终止
迭代器版filter的最大实战价值在于惰性带来的融合(fusion)效果。由于filter返回的是惰性迭代器,当它处于pipe中间时,整个管道不会为每个步骤分别遍历一遍数据,而是对每个元素“边流经各步骤边判断”,并且可以在下游提前终止时停止上游生产。
虽然 src/fp/array/filter.spec.ts 中的融合用例是针对数组版验证的,但它揭示的管道语义同样适用于迭代器版:map、filter、take串成的管道,mapSpy与filterSpy都只被调用了 4 次(而不是 8 次),因为take(2)拿到前两个合格结果后就终止了整条流水线。
把同样的思路用在迭代器版上,你可以构建出“无需物化中间数组”的长管道:
import { pipe } from 'es-toolkit/fp'; import { filter, map, take, toArray } from 'es-toolkit/fp/iterator'; // 对 1 到 100 的迭代器:求平方 → 筛偶数 → 取前 3 个 const result = pipe( range(1, 101).values(), // 伪代码示意:任意迭代器数据源 map(x => x * x), filter(x => x % 2 === 0), take(3), toArray() );由于每一步都保持惰性,源迭代器最多只会被推进到产生 3 个合格结果所需的元素数,而不是先完整算完 100 个平方数再筛选。
使用建议:何时选原生、何时选本函数
结合官方文档的提示与源码实现,可以总结出清晰的选择规则:
- 普通代码、单次筛选:直接用原生
source.filter(predicate),无需额外依赖,性能最优。 - 用
pipe组合多个转换:使用es-toolkit/fp/iterator的filter,让每一步都以“接收迭代器、返回迭代器”的纯函数形态接入管道,代码可读性与可组合性最佳。 - 需要类型收窄:两种方式都支持类型守卫,但本函数的柯里化形态配合
pipe时,类型信息能在整条管道中正确流动。
filter还可以与 src/fp/iterator/index.ts 中同系列的其他函数(map、take、takeWhile、dropWhile、partition、uniqBy、zip、count、scan等)自由组合,构成完整的惰性数据处理工具箱。
小结
es-toolkit/fp/iterator的filter是一个“薄而准”的函数式适配层:它以柯里化形态把原生Iterator.prototype.filter接入pipe管道,通过Iterator.from保证任意迭代器源的兼容性,通过双重重载在编译期为类型守卫提供收窄能力,并以惰性求值天然支持管道融合与提前终止。对于任何需要在函数式管道中处理无限或大型迭代数据流的场景,它都是衔接es-toolkit/fp与 ES 原生迭代器能力的关键一环。
- 官方文档(日文):docs/ja/iterator/reference/filter.md
- 官方文档(英文):docs/iterator/reference/filter.md
- 源码实现:src/fp/iterator/filter.ts
- 同系列导出:src/fp/iterator/index.ts
- 数组版对比实现:src/fp/array/filter.ts
- 数组版行为测试:src/fp/array/filter.spec.ts
pipe使用指南:docs/ja/fp/reference/pipe.md
【免费下载链接】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),仅供参考