es-toolkit fp 模块 windowed 实战:在 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 函数式编程入口es-toolkit/fp中的windowed算子展开,讲解如何用pipe(array, windowed(size, step, options))的方式对数组做滑动窗口切分,并深入源码剖析其"全窗口模式可在pipe内惰性求值、可提前终止"的实现机制。读完本文,你将掌握该算子的全部参数、边界行为与类型签名,并能在时间序列、n-gram 提取等场景中写出既简洁又高效的管道式代码。
windowed 是什么:为 pipe 而生的滑动窗口算子
windowed会从输入数组中切出若干个长度固定的"窗口"子数组,窗口沿着数组按固定步长滑动,从而形成一段连续的"快照"序列。这是时间序列分析(如移动平均)、字符串 n-gram 提取、数组模式匹配等任务的基础操作。
在 es-toolkit 中该功能有两个版本:
- 普通版
windowed(位于es-toolkit/array):data-first 风格,直接接收数组作为第一个参数; - fp 版
windowed(本文主题):data-last 风格,先接收size、step、options等配置,返回一个"把readonly T[]映射为T[][]"的函数,专门与函数式组合入口pipe搭配使用:
import { pipe, windowed } from 'es-toolkit/fp'; const result = pipe(array, windowed(size, step, options));这种设计让windowed可以像map、filter、take一样作为一等算子嵌入管道,与其他变换任意串联。若你的代码并不需要管道组合,官方文档明确建议在普通代码中使用>import { pipe, windowed } from 'es-toolkit/fp'; // 窗口大小为 2,默认步长 1:相邻两两成组 pipe([1, 2, 3, 4], windowed(2)); // => [[1, 2], [2, 3], [3, 4]] // 窗口大小为 2,步长 2:不重叠切分 pipe([1, 2, 3, 4], windowed(2, 2)); // => [[1, 2], [3, 4]]
第一个示例中窗口每次前进 1 位,产生重叠窗口;第二个示例中窗口每次前进 2 位,恰好与窗口等宽,行为等同于"按块切分"。这与 src/fp/array/windowed.spec.ts 中的首个测试用例(works in a pipe)完全一致。
参数详解
fp 版windowed的函数签名为windowed(size, step?, options?),三个参数含义如下:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
size | number | 是 | — | 每个窗口的长度,必须是正整数 |
step | number | 否 | 1 | 相邻两个窗口起点之间前进的位置数,必须是正整数 |
options | WindowedOptions | 否 | {} | 配置项,目前仅包含partialWindows |
options.partialWindows(boolean,默认false)决定是否把数组末尾凑不齐一个完整窗口的"残缺窗口"也一并返回。在 src/array/windowed.ts 中定义的WindowedOptions接口即:
export interface WindowedOptions { /** * Whether to include partial windows at the end of the array. * 默认只返回完整窗口,末尾不足以凑成完整窗口的元素会被忽略; * 设为 true 后,末尾这些更小的窗口也会被包含进结果。 */ partialWindows?: boolean; }再看带partialWindows的管道示例(与 src/fp/array/windowed.spec.ts 的第三个测试一致):
pipe([1, 2, 3], windowed(2, 2, { partialWindows: true })); // => [[1, 2], [3]] // 末尾的 [3] 是不足 2 的残缺窗口返回类型
windowed(size, step?, options?)返回一个函数,其类型为:
(array: readonly T[]) => T[][]即:接收一个只读数组,返回由多个窗口子数组组成的二维数组。每个窗口都是原数组元素的"快照",底层通过slice产生新的子数组(见 src/array/windowed.ts),因此修改窗口内容不会影响原数组。
错误处理
当size或step不是正整数(小于等于 0 或非整数)时,windowed会抛出Error。核心实现位于 src/array/windowed.ts:
if (size <= 0 || !Number.isInteger(size)) { throw new Error('Size must be a positive integer.'); } if (step <= 0 || !Number.isInteger(step)) { throw new Error('Step must be a positive integer.'); }src/array/windowed.spec.ts 的测试用例覆盖了全部非法输入:size = 0、step = 0、负数、小数(如0.5)都会触发对应报错,例如windowed([1, 2, 3], 0)抛出Error: Size must be a positive integer.。
行为边界:结合测试看窗口语义
普通版windowed的单元测试(src/array/windowed.spec.ts)完整刻画了滑动窗口的边界行为,这些语义同样适用于 fp 版:
- 步长小于窗口:产生重叠窗口,如
windowed([1..6], 3, 2)得到[[1,2,3],[3,4,5]]; - 步长等于窗口:退化为不重叠分块,如
windowed([1..6], 3, 3)得到[[1,2,3],[4,5,6]],与chunk行为一致; - 步长大于窗口:会跳过部分元素,如
windowed([1..6], 2, 4)得到[[1,2],[5,6]]; - 步长超过数组长度:只产生第一个窗口,如
windowed([1..6], 2, 10)得到[[1,2]]; - 空数组:返回空数组
[]; - 窗口大于数组且开启
partialWindows:返回逐级缩短的残缺窗口,如windowed([1,2], 5, 1, { partialWindows: true })得到[[1,2],[2]](见 docs/reference/array/windowed.md 示例)。
源码深潜:全窗口模式如何在 pipe 中惰性求值
fp 版windowed最大的亮点是:只返回完整窗口(默认模式)时,它在pipe内部具备惰性求值能力,可以和相邻的map、filter、take等算子融合成单趟扫描,并支持提前终止。这一能力由 src/fp/_internal/lazy.ts 提供的基础设施支撑。
入口:eager 与 lazy 两条路径的分流
src/fp/array/windowed.ts 的入口逻辑如下:
const resolvedStep = step ?? 1; const partialWindows = options?.partialWindows ?? false; function windowedEager(array: readonly T[]): T[][] { return windowedToolkit(array, size, step, options); } if (partialWindows || !Number.isInteger(size) || size <= 0 || !Number.isInteger(resolvedStep) || resolvedStep <= 0) { return windowedEager; // 走即时计算 } const windowedLazy = (emit: Sink<T[]>): Sink<T> => { /* ... */ }; return combineEagerAndLazyFunctions(windowedEager, windowedLazy);可见,当开启partialWindows或参数非法时,直接返回 eager 版本(即时计算,以保证残缺窗口和错误抛出语义);只有在"纯全窗口 + 合法参数"的默认场景下,才附加惰性变换。eager 版本内部委托给原版实现windowedToolkit,即 src/array/windowed.ts 中基于for循环与arr.slice(i, i + size)的实现。
惰性实现:push 式流水线 + 环形缓冲
windowedLazy是一个典型的"push 式"惰性变换(详见 src/fp/_internal/lazy.ts 的文档注释):它不是用生成器,而是每个函数接收下一级 sink,通过emit把值"推"下去;pipe负责把这些变换按从后往前的顺序组合成一条 sink 链,再用一个循环驱动整条链。其核心片段:
const buffer: T[] = []; let index = 0; return (value: T): boolean => { buffer.push(value); if (buffer.length > size) { buffer.shift(); // 维持缓冲区长度不超过 size } if (buffer.length === size) { const start = index - size + 1; if (start % resolvedStep === 0) { const shouldContinue = emit(buffer.slice()); // 推一个窗口快照 index++; return shouldContinue; } } index++; return true; };这里用了一个长度恒定的滑动缓冲:每来一个元素先入队,超出size就从头部弹出,再用start % resolvedStep === 0判断当前窗口起点是否落在步长网格上,命中才把buffer.slice()(拷贝快照)交给下一级。返回false表示整条管道提前结束,从而避免继续遍历剩余输入。
pipe 如何驱动:融合与短路
在 src/fp/pipe.ts 的实现中,pipe会把连续的惰性算子聚合成一组(chunkFunctions),当输入是可迭代对象时走lazyPipe:从最后一个函数开始,把每个函数的lazy变换层层包裹成最终 sink,然后对输入逐元素驱动;一旦某级 sink 返回false(如take的短路),驱动循环立即break。
windowed正是通过combineEagerAndLazyFunctions(windowedEager, windowedLazy)(见 src/fp/_internal/lazy.ts)把 eager 实现和惰性变换打包成一个带lazy元数据的算子函数返回给调用方,pipe据此识别并融合它。
测试如何验证惰性
src/fp/array/windowed.spec.ts 用 spy 严格验证了惰性语义:
it('supports lazy evaluation for full windows', () => { const spy = vi.fn((value: number) => value); expect(pipe([1, 2, 3, 4], map(spy), windowed(2), take(1))).toEqual([[1, 2]]); expect(spy).toHaveBeenCalledTimes(2); });map(spy)之后接windowed(2)再接take(1):因为只需要第一个窗口,map的回调实际只被调用了2 次(恰好凑出一个窗口)就停止,剩余元素完全没有被访问。这就是"融合 + 提前终止"的直接证据:如果不走惰性路径,map会先对整个数组执行完 4 次回调,再交给后面的算子。
使用建议与典型场景
- 何时用 fp 版:在
pipe/flow中与其他算子组合时使用 fp 版;普通命令式代码建议直接用原版windowed。 - 何时会失去惰性:一旦开启
partialWindows: true,为了保留末尾残缺窗口,fp 版会自动退回 eager 即时计算路径(见上文分流逻辑),管道中位于其前的算子将无法与该段融合短路。因此追求极致性能时应优先考虑纯全窗口模式。 - 典型应用:结合窗口语义可以轻松实现移动平均(对
windowed(数据, n)的每个窗口求均值)、文本 n-gram 提取(对分词后的字符串数组做windowed(n))、以及各类滑窗检测算法;配合pipe还可继续用map、filter、take等算子做后续加工。
小结
es-toolkit fp 版的windowed是一个典型的 contenteditable="false">【免费下载链接】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),仅供参考