Kodokon kodokon.com

迭代器、生成器和迭代协议

掌握 for...of、展开运算符和解构背后的协议,然后用生成器来产生惰性序列。

9 分钟 · 3 题

在 Kodokon 中打开本课

有两个不同的协议管辖着迭代。如果一个对象在 Symbol.iterator 键下暴露了一个方法,它就是可迭代的;该方法必须返回一个迭代器,也就是一个带有 next() 方法、返回 { value, done } 的对象。所有消费序列的东西——for...of、展开运算符 ...、解构、Array.fromPromise.all——都要经过这些协议。因此你可以让任何对象都变得兼容。

JAVASCRIPT
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3]
一个手写的可迭代对象,没有用到类或生成器。

手写迭代器很啰嗦。生成器function*)会自动产生一个迭代器:每个 yield 都会挂起执行并保留函数的局部状态。这种惰性求值让你能够对无限序列进行建模,只要有一个消费方限制所请求值的数量即可。

JAVASCRIPT
function* naturals() {
  let n = 0;
  while (true) yield n++;
}

function* take(iterable, count) {
  let index = 0;
  for (const value of iterable) {
    if (index >= count) return;
    yield value;
    index += 1;
  }
}

console.log([...take(naturals(), 4)]); // [0, 1, 2, 3]
一个无限序列在完全不阻塞的情况下被消费。

生成器迭代器还会暴露 return()throw()。有一个常被忽视的规范细节:当你通过 breakreturn 或抛出异常离开 for...of 循环时,引擎会自动调用 iterator.return()。在生成器中,这会运行任何挂起的 finally 块——这是释放资源(文件、连接、锁)的理想位置。

JAVASCRIPT
function* readLines() {
  try {
    yield "line 1";
    yield "line 2";
  } finally {
    console.log("cleanup");
  }
}

for (const line of readLines()) {
  console.log(line);
  break; // triggers return(), hence the finally
}
// line 1, cleanup
break 会调用 return():finally 块随之运行。

知识检测

确认你已牢记本课的重点内容。

  1. 在协议的意义上,是什么让一个对象变得可迭代?
    • 拥有一个 length 属性和数字索引
    • 暴露一个返回带 next() 对象的 Symbol.iterator 方法
    • 继承自 Array.prototype
    • 直接拥有一个 next() 方法
  2. 当你用 break 退出一个正在消费生成器的 for...of 循环时,会发生什么?
    • 没什么特别的,生成器停留在最后一个 yield 处保持挂起
    • 引擎会在迭代器上调用 throw()
    • 引擎会调用 return(),从而运行生成器的 finally
    • 生成器会在下一次迭代时从头重新开始
  3. 为什么你可以在不阻塞程序的情况下使用一个无限生成器?
    • 引擎会自动限制迭代次数
    • 生成器在一个单独的线程上运行
    • 求值是惰性的:每个值只有在调用 next() 时才会被计算
    • 这些值由引擎预先计算并缓存