7.1 迭代器模式

7.1.1 可迭代协议

实现Iterator 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator 接口的对象的能力。在ECMAScript中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator作为键。该默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

ES6+ 中许多内置类型都默认实现了可迭代协议。
  • Array
  • String
  • Map
  • Set
  • arguments 对象
  • NodeList 等 DOM 集合类型
let num = 1;
let obj = {};
//这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]);//undefined
console.log(obj[Symbol.iterator]);//undefined

let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
//这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]);//[Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]);//[Function: values]
console.log(map[Symbol.iterator]);//[Function: entries]
console.log(set[Symbol.iterator]);//[Function: values]
//调用迭代器工厂函数,返回迭代器对象
console.log(str[Symbol.iterator]());//Object [String Iterator] {}
console.log(arr[Symbol.iterator]());//Object [Array Iterator] {}
console.log(map[Symbol.iterator]());//[Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] } 
console.log(set[Symbol.iterator]());//[Set Iterator] { 'a', 'b', 'c' }
接收可迭代对象的原生语言特性包括:
  • for-of循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收由期约组成的可迭代对象
  • Promise.race()接收由期约组成的可迭代对象
  • yield*操作符,在生成器中使用。

这些原生语言结构会在后台调用提供的可迭代对象的工厂函数,从而创建一个迭代器。如果对象原型链上的父类实现了Iterator 接口,那么该对象也就实现了该接口。

let arr = ['foo', 'bar', 'baz'];
// for...of 循环本质上会做:
// 1. 调用 arr[Symbol.iterator]() 获取迭代器
// 2. 反复调用迭代器的 .next() 方法
// 3. 将返回对象的 value 赋给变量,直到 done 为 true
for (let el of arr) {
  console.log(el); // 'foo', 'bar', 'baz'
}
//数组解构
let [a,b,c] = arr;
console.log(a, b, c);// 'foo', 'bar', 'baz'
7.1.2 迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next()都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。

next() 方法返回一个包含以下两个属性的IteratorResult对象:

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next()都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。

  • value: 当前迭代的值。可以是任何类型。
  • done: 一个布尔值。表示是否还可以再次调用next() 会返回下一个值;
    • false 表示迭代尚未完成,下次调用 next() 会返回下一个值。
    • true 表示迭代已结束。此时 value 属性可以省略,如果存在则被认为是迭代器的“返回值”,但 for...of 循环会忽略它。
let arr = ['foo', 'bar', 'baz']
console.log(arr[Symbol.iterator]);//[Function: values]
//创建两个迭代器
let iter = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();
console.log(iter.next());//{ value: 'foo', done: false }
console.log(iter.next());//{ value: 'bar', done: false }
console.log(iter.next());//{ value: 'baz', done: false }
//只要迭代器到达done:true,后续的next()调用都返回相同的值
console.log(iter.next());//{ value: undefined, done: true }
console.log(iter.next());//{ value: undefined, done: true }
//两个迭代器是相互独立的,只会独立的进行迭代
console.log(iter2.next());//{ value: 'foo', done: false }
arr.splice(1,0,'qux');
//如果迭代对象在迭代期间被修改了,迭代器也会反映相应的变化
console.log(iter2.next());//{ value: 'qux', done: false }
console.log(iter2.next());//{ value: 'bar', done: false }

注意:迭代器维护着一个指向可迭代对象的引用,因此迭代器会组织垃圾回收程序回收可迭代对象。

7.1.3 提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。

提前关闭可能的情况包括:
  • for-of循环通过break、continue、return或throw提前退出;
  • 解构操作并未消费所有值

return()方法必须返回一个有效的IteratorResult对象,简单情况下可以只返回{done: true}

class Counter {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator](){
    let count = 1,
        limit = this.limit;
    return {
      next() {
        if (count <= limit) {
          return { value: count++, done: false};
        } else {
          return { done: true };
        }
      },
      return(){
        console.log('Exiting early');
        return {done: true};
      }
    };
  }
}
let counter = new Counter(5);
for (let value of counter) {
  console.log(value);
  if (value === 2) {
    break;
  }
}
//1
//2
//Exiting early
let counter2 = new Counter(5);
try{
  for (let value of counter2) {
    console.log(value);
    if (value === 2) {
      throw 'error';
    }
  }
}catch(e){
}
//1
//2
//Exiting early
let [a,b,c] = new Counter(5);
//Exiting early

如果迭代器没有关闭,则还可以从上次离开的位置继续迭代。比如数组的迭代器就无法关闭:

let a = [1,2,3,4,5]
let iter = a[Symbol.iterator]()
for(let i of iter){
    console.log(i)
    if(i > 2){
      break;
    }
}
//1
//2
//3
for(let i of iter){
    console.log(i)
}
//4
//5

因为return()方法是可选的,所有并非所有迭代器都是可以关闭的。要知道某个迭代器是不是可以关闭的,可以测试该迭代器实例的return属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加一个return()方法并不能使其变成可关闭的。但是return()方法会被调用,但无法强制关闭。

7.2 生成器

生成器是ECMAScript6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

7.2.1 生成器基础

生成器的形式是一个函数,函数名称前加一个星号(*)表示它是一个生成器,标识生成器函数的星号不受两侧空格的影响。只要是可以定义函数的地方就可以定义生成器,但是箭头函数不能用来定义生成器函数。

//生成器函数声明
function* generatorFn(){}
//作为函数调用表达式的生成器函数
let generatorFn1 = function*(){}
//作为对象属性的生成器函数
let foo = {
  *generatorFn2(){}
}
//作为类实例方法的生成器函数
class Foo{
  *generatorFn3(){}
}
//作为静态方法的生成器函数
class Bar {
  static *generatorFn4(){}
}

调用生成器函数并不会立即执行其函数体,而是返回一个生成器对象。这个生成器对象既符合可迭代协议(拥有 [Symbol.iterator] 方法),也符合迭代器协议(拥有 next() 方法)。生成器对象一开始处于暂停状态(suspended),通过next()方法可以让生成器开始或恢复执行。


function* generatorFn(){}
let generatorObj = generatorFn();
console.log(generatorObj);//Object [Generator] {}
console.log(generatorFn()[Symbol.iterator]);//[Function: [Symbol.iterator]]
//生成器对象实现了Iterator接口,它们默认的迭代器是自引用的。
console.log(generatorObj === generatorObj[Symbol.iterator]());//true
console.log(generatorObj.next());//{ value: undefined, done: true }

7.2.2 通过yield中断执行

yield 关键字:在生成器函数内部使用。它会暂停函数的执行,并将紧跟在 yield 后面的值作为 next() 返回对象的 value。下一次调用 next() 时,函数会从上次暂停的地方继续执行。通过yield关键字退出的生成器函数会处于done:false状态,通过return关键字退出的生成器函数会处于done:true状态。生成器函数内部的执行流程会针对每个生成器对象区分作用域,相互直接不影响。

function* generatorFn() {
  yield 'foo';
  yield 'bar';
  return 'baz'; // return 的值会在 done 为 true 时作为 value
}

const generatorObject = generatorFn();

console.log(generatorObject.next()); // { value: 'foo', done: false }
console.log(generatorObject.next()); // { value: 'bar', done: false }
console.log(generatorObject.next()); // { value: 'baz', done: true }

// 因为生成器对象也是可迭代的,所以可以直接用 for...of
for (const item of generatorFn()) {
  console.log(item); // 'foo', 'bar' (注意:for...of 不会消费 'baz',因为遇到 done: true 就停止了)
}
生成器的控制权交换

yield 本质上是双向的。它不仅能把值传出去,还能通过 next(val) 方法接收外部传入的值,这使得生成器可以用来实现复杂的协程(Coroutine)模式。

function* generatorFn(){
  return yield 'foo';
}
let generatorObj = generatorFn();
console.log(generatorObj.next());//{value: 'foo', done: false}
console.log(generatorObj.next('bar'));//{value: 'bar', done: true}

function* nTimes(n){
  let i = 0;
  while(n--){
    yield i++;
  }
}
for(let x of nTimes(3)){
  console.log(x);
}
//0
//1
//2
//实现一个range函数,返回一个序列,从start开始到end结束,每次增加step,默认step为1
function* range(start, end){
  while(start < end){
    yield start++;
  }
}
for(let x of range(4, 7)){
  console.log(x);
}
//4
//5
//6
产生可迭代对象:

可以使用星号增强yield行为,让它能够迭代一个可迭代对象,从而一次只产出一个值。

function* generatorFn() {
  yield* [1, 2, 3];
}
for (const value of generatorFn()) {
  console.log(value);
}
//1
//2
//3
使用yield*实现递归算法:
function* nTimes(n) {
  if (n > 0) {
    yield* nTimes(n - 1);
    yield n-1;
  }
}
for (const value of nTimes(3)) {
  console.log(value);
}
//0
//1
//2
生成器作为默认迭代器
class Foo {
  constructor(){
    this.values = [1,2,3]
  }
  *[Symbol.iterator]() {
    yield* this.values;
  }
}
const foo = new Foo();
for (const value of foo) {
  console.log(value);
}
//1
//2
//3

7.2.3 提前终止生成器

生成器除了next()方法,还有一个可选的return()方法和throw()方法。都可以用于强制生成器进入关闭状态。

1、return()

该方法强制生成器进入关闭状态。提供给return()的值就是终止迭代器对象的值。与迭代器不同,所有生成器对象都有return()方法,一旦通过它进入关闭状态就无法恢复。

function* generatorFn(){
   yield* [1,2,3];
}
const g = generatorFn();
console.log(g.next());//{value:1,done:false}
console.log(g.return(4));//{value:4,done:true}
console.log(g.next());//{value:undefined,done:true}
2、throw()

该方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器会关闭。

function* generatorFn(){
  for(let i=0;i<3;i++){
    yield i;
  }
}
const g = generatorFn();
console.log(g.next());//{ value: 0, done: false }
try{
  g.throw('foo');
}catch(e){
  console.log(e);//foo
}
console.log(g.next());//{ value: undefined, done: true }

不过如果生成器函数内部处理了这个错误,那么生成器就不会关闭,可以恢复执行。但是错误处理会跳过对应的yield。

function* generatorFn(){
  for(let i=0;i<3;i++){
    try{
      yield i;
    }catch(e){
      console.log(e);
    }
  }
}
const g = generatorFn();
console.log(g.next());//{ value: 0, done: false }
g.throw('foo');//foo
console.log(g.next());//{ value: 2, done: false }

7.3 异步迭代

ES2018 引入了异步迭代器for-await-of 循环,用于处理异步数据流。

1. 异步迭代协议

它与同步迭代协议类似,但有两点关键区别:

  • 可迭代协议:对象实现 [Symbol.asyncIterator] 方法(而不是 [Symbol.iterator])。
  • 迭代器协议:该迭代器的 next() 方法返回一个 Promise。这个 Promise 解决后,其值是一个形如 { value, done } 的对象。
2. for-await-of 循环

这个循环专门用于消费异步可迭代对象。它会在每次迭代中 await 迭代器 next() 方法返回的 Promise。

// 假设我们有一个函数,它分页获取数据,每一页是一个异步请求
async function* asyncPaginatedFetcher() {
  let page = 1;
  while (true) {
    // `fetch` 返回一个 Promise
    const response = await fetch(`/api/data?page=${page}`);
    const data = await response.json();

    if (data.length === 0) {
      break; // 没有更多数据了
    }

    for (const item of data) {
      // 逐个产出数据项,但每次 next() 调用都是异步的
      yield item;
    }
    page++;
  }
}

// 使用 for-await-of 消费这个异步生成器
(async () => {
  for await (const dataItem of asyncPaginatedFetcher()) {
    console.log(dataItem);
    // 循环会自动等待每一页数据的获取和每一个项的产出
    // 实现了以同步写法处理异步数据流
  }
})();
3. 应用场景
  • 分页获取所有数据:如上例所示。
  • 处理 Node.js 流:Node.js 的 readable 流在 objectMode 或使用 stream.values() 后,可以作为异步可迭代对象使用。
  • WebSocket 消息:可以创建一个异步生成器来持续接收 WebSocket 消息。
  • RxJS Observables:虽然 Observables 本身不是异步可迭代对象,但可以很容易地适配。

7.4 总结

概念

核心思想

现代应用

迭代器协议

定义了如何产生一个值序列(next()

方法)。

所有集合操作、解构、扩展运算符的基础。

可迭代协议

定义了如何获取一个迭代器([Symbol.iterator]

方法)。

让自定义数据结构(如链表、树)能够与 JavaScript 原生语法(for...of

, ...

)无缝集成。

生成器

通过 function*

yield

简化迭代器的创建。提供了暂停/恢复的能力。

Redux Saga 等库用其管理复杂异步流;实现无限序列;协程。

异步迭代

迭代器 next()

返回 Promise,用于处理异步数据流。

消费分页 API、Node.js 流、实时消息流(for-await-of

)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐