生成器要分块只能手写指针?这个写了五年的模式,今天被 Iterator.chunks() 和 windows() 彻底接上了

写过生成器的人都知道,生成器最擅长的场景是处理大数据集——不用把整个数据集 Load 进内存,一段一段吐出来,边吐边处理。但这里有个问题:当你需要对生成器的内容做「分块」或「滑动窗口」操作时,以前只能自己写指针逻辑,没有语言层面的支持。

今天这事儿彻底变了。


旧方法:手写分块逻辑

假设你有一个生成器,吐出斐波那契数列,你想每 5 个数字打印成一行:

function* fibonacci() {
  let current = 1, next = 1;
  while (true) {
    yield current;
    [current, next] = [next, current + next];
  }
}

在有 chunks() 之前,你只能这样写:

function chunk(iterator, size) {
  const chunk = [];
  for (const item of iterator) {
    chunk.push(item);
    if (chunk.length === size) {
      yield chunk;
      chunk = [];
    }
  }
  if (chunk.length > 0) yield chunk; // 处理剩余
}

你自己维护了一个临时数组,每满 size 个就 yield 一块,最后还要处理尾部。这是一个你可能写过几十遍的模式。

滑动窗口更难——你需要维护一个固定大小的队列,每次 push 一个、shift 一个:

function slidingWindow(iterator, size) {
  const window = [];
  for (const item of iterator) {
    window.push(item);
    if (window.length > size) window.shift();
    if (window.length === size) yield [...window];
  }
}

这段代码逻辑不难,但容易写错、边界情况多、且难以复用。


新方法:chunks() 和 windows()

ES2026 的 Iterator Sequencing 提案(TC39 Stage 4)给你两个新方法:

function* fibonacci() {
  let current = 1, next = 1;
  while (true) {
    yield current;
    [current, next] = [next, current + next];
  }
}

// 分块:每5个一组
const rows = fibonacci().chunks(5);
for (const row of rows.take(5)) {
  console.log(row.join('t'));
}
// 输出:
// 1    1    2    3    5
// 8    13   21   34   55
// ...

// 滑动窗口:固定大小窗口,每次向右滑动一格
const windows = fibonacci().windows(3);
for (const win of windows.take(5)) {
  console.log(win.join(', '));
}
// 输出:
// 1, 1, 2
// 1, 2, 3
// 2, 3, 5
// 3, 5, 8
// 5, 8, 13

chunks(n) 把迭代器的内容切成每 n 个一组,组与组之间不重叠;windows(n) 则是滑动窗口,每前进一次窗口向右移动一格,窗口内容更新头尾两端。

两个方法都返回惰性的迭代器助手(iterator helper),不触发任何中间数组分配。


内存效率的差距

为什么这件事值得原生化?来看一个具体场景:

处理 100 万条记录,每批处理 1000 条:

// 旧方法:先全部消费到数组再分块
const records = fetchRecords(); // 100万条
const batches = chunk(records, 1000); // chunk 返回生成器,可以
for (const batch of batches) {
  process(batch); // 每批1000条
}

这个看起来还行,但如果配合 .take() 提前截止呢:

// chunks().take(3):只取前3块,chunks() 的惰性特性保证只调用3次 next()
for (const batch of fibonacci().chunks(5).take(3)) {
  console.log(batch);
}
// 输出:[1,1,2,3,5] [8,13,21,34,55] [89,144,233,377,610]
// 生成器只被调用了 3 次

这是惰性迭代的核心优势:需要多少,就处理多少。


滑动窗口的实际用途

windows() 在数据处理场景里特别有用。比如你要计算移动平均值:

// 斐波那契数列的相邻两项滑动平均值
const fib = fibonacci();
const movingAvg = fib
  .windows(2)
  .map(([a, b]) => (a + b) / 2);

console.log(movingAvg.next().value); // 1.0   (1+1)/2
console.log(movingAvg.next().value); // 1.5   (1+2)/2
console.log(movingAvg.next().value); // 2.5   (2+3)/2
console.log(movingAvg.next().value); // 3.666 (3+5)/2

在信号处理、统计分析、时序数据展示等场景里,移动窗口是常见模式。以前需要自己维护数组,现在一个 .windows(n) 搞定。


浏览器兼容性

特性 Chrome Firefox Safari Node.js Bun Deno
Iterator.chunks() — 154+ TP — 1.4+ —
Iterator.windows() — 154+ TP — 1.4+ —

Firefox 154 是目前最完整的实现,Bun 1.4 也已支持。Chrome/Safari/Node.js 尚未默认开启,但基于 Iterator Helpers 已进 Baseline 2026(2026 年 9 月全面可用),这两者的跟进只是时间问题。


三步迁移路径

第一步:替换手写分块

检查你代码库里是否有 chunk / groupBy / partition 类型的手写函数,尤其输入是生成器或异步迭代器的:

// Before
function chunk(iterable, size) { ... }
for (const batch of chunk(myGenerator(), 100)) { ... }

// After
for (const batch of myGenerator().chunks(100)) { ... }

第二步:滑动窗口场景

搜索 while 循环内同时有 .push() 和 .shift() 的模式,这几乎总是滑动窗口的变体:

// Before:手写滑动窗口
const window = [];
for (const item of source) {
  window.push(item);
  if (window.length > size) window.shift();
  if (window.length === size) process(window);
}

// After
for (const window of source.windows(size)) {
  process(window);
}

第三步:结合其他迭代器助手

chunks() 和 windows() 可以链式调用 .map()、.filter()、.take() 等,形成完整的惰性数据处理流水线:

source
  .chunks(1000)
  .filter(chunk => chunk.some(item => item.active))
  .map(chunk => chunk.map(item => item.id))
  .take(10)
  .toArray()

全程无中间数组分配,内存占用稳定。


总结

Iterator.chunks() 和 windows() 填补了生成器世界最后一块空白——把「分块」和「滑动窗口」这两个在数据处理中无处不在的模式从手写逻辑带进了语言层。结合 Iterator.concat()(将多个迭代器首尾相连)和 Iterator.zip()(并行遍历),Iterator Sequencing 提案已经构建起一套完整的惰性迭代工具箱。在 Firefox 154+ 和 Bun 1.4+ 上已经可用,值得你现在就开始替换手写实现。


参考来源:

评论区

0 条评论

登录后可评论。

阿柯·前端架构 144 阅读