news 2026/9/16 18:24:58

es-toolkit fp 模块 windowed 实战:在 pipe 中实现滑动窗口与惰性求值

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
es-toolkit fp 模块 windowed 实战:在 pipe 中实现滑动窗口与惰性求值

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 风格,先接收sizestepoptions等配置,返回一个"把readonly T[]映射为T[][]"的函数,专门与函数式组合入口pipe搭配使用:
import { pipe, windowed } from 'es-toolkit/fp'; const result = pipe(array, windowed(size, step, options));

这种设计让windowed可以像mapfiltertake一样作为一等算子嵌入管道,与其他变换任意串联。若你的代码并不需要管道组合,官方文档明确建议在普通代码中使用>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?),三个参数含义如下:

参数类型必填默认值说明
sizenumber每个窗口的长度,必须是正整数
stepnumber1相邻两个窗口起点之间前进的位置数,必须是正整数
optionsWindowedOptions{}配置项,目前仅包含partialWindows

options.partialWindowsboolean,默认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),因此修改窗口内容不会影响原数组。

错误处理

sizestep不是正整数(小于等于 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 = 0step = 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内部具备惰性求值能力,可以和相邻的mapfiltertake等算子融合成单趟扫描,并支持提前终止。这一能力由 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还可继续用mapfiltertake等算子做后续加工。

小结

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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 18:24:42

ThinkPHP6淘宝礼品代发系统全链路实现

简介&#xff1a;这是一套基于ThinkPHP框架开发的礼品代发与淘宝一件代发业务系统源码&#xff0c;面向电商创业者、中小代发平台开发者及PHP中级以上技术人员&#xff0c;旨在解决礼品类商家无库存运营、订单自动同步、多渠道发货协同等核心痛点。资源包共82个文件&#xff0c…

作者头像 李华
网站建设 2026/9/16 18:24:27

Avalonia XAML字符串处理:x:String与CDATA实战技巧

1. Avalonia XAML 字符串处理痛点解析在 Avalonia 的 XAML 开发中&#xff0c;处理复杂字符串一直是个令人头疼的问题。我最近在重构一个跨平台音乐播放器项目时&#xff0c;就遇到了 XML 特殊字符与格式化文本的冲突问题。当需要在界面中嵌入包含尖括号、引号或特殊符号的字符…

作者头像 李华
网站建设 2026/9/16 18:22:06

C++实现光学测量模拟系统:格雷码与多频外差法

1. 项目背景与核心价值在工业检测和三维重建领域&#xff0c;光学测量技术因其非接触、高精度的特性成为关键解决方案。这个C实现的光学测量模拟系统&#xff0c;聚焦于两种主流编码技术——格雷码相位移和多频外差法的完整实现流程。我在自动化检测设备开发中多次应用这类技术…

作者头像 李华
网站建设 2026/9/16 18:19:45

MEMS麦克风与耗尽型音频开关协同设计指南

1. 项目概述&#xff1a;从一颗MEMS麦克风和一颗音频开关说起你拆开手头那台老款会议终端、工业录音设备&#xff0c;或者某款国产智能音箱的主板&#xff0c;大概率会在音频输入通路附近看到两颗不起眼的黑色小芯片&#xff1a;一颗标着SPQ0410HR5H-B&#xff0c;另一颗印着R7…

作者头像 李华