迭代器和生成器

188 阅读9分钟

迭代的英文“iteration”源自拉丁文itero,意思是“重复”或“再来”。在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。ECMAScript 6规范新增了两个高级特性:迭代器和生成器。使用这两个特性可以更清晰、高效、方便地实现迭代。

——《JavaScript高级程序设计(第四版)》

迭代器

循环是迭代机制的基础,迭代会在一个有序集合上进行。(集合中所有项都按照顺序被遍历到,特别是开始和结束项有明确定义)数组是JavaScript中有序集合最典型的例子。

为什么有了循环还要迭代器呢?

  • 循环前需要知道当前数据结构如何取值。数组 可以通过[]操作符取得值,这个方式并不适合其他数据结构,例如 SetMap
  • 遍历顺序并不是数据结构固有的。 通过递增索引来访问数据是数组类型方式,并不适用于其它具有隐式顺序的数据结构。比如字符串

很多原生语言开发者无须事先知道如何迭代就能实现迭代操作,这个解决方案就是迭代器模式 。JavaScript在ES6以后也支持了迭代器模式。

迭代器模式:这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。为遍历不同数据结构提供统一接口。

迭代器模式属于行为型模式。

可迭代协议

可以把有些结构成为“可迭代对象”(iterable),因为它们实现了正式的Iterable接口,可以通过迭代器(iterator)消费(consume)。迭代器是按需创建的一次性对象,每个 迭代器 都会关联一个 可迭代对象

可以把可迭代对象理解成为数组(Array)或者集合(Set)这样集合类型的对象 。

在JavaScript中创建实现Iterator接口属性名为 [Symbol.iterator] 。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新的迭代器。

有些内置类型实现了Iterator接口:

  • 字符串(string)
  • 数组(Array)
  • 映射(Map)
  • 集合(Set)
  • arguments对象(函数参数)
  • NodeList等DOM集合类型

在实际写代码中不需要显式调用这个工厂函数来生成迭代器,根据语言特性会在后台自动调用工厂函数来生成迭代器,这些特性包括:

  • for-of循环
  • 数据解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收异步函数组成可迭代对象
  • Promise.race()接收异步函数组成可迭代对象
  • yield * 操作符
const set = new Set().add(1).add(2).add(3)
const obj = {}
​
console.log(set) // Set(3) { 1, 2, 3 }
console.log(obj) // {}// 检查是否实现 iterator 接口
console.log(set[Symbol.iterator]) // ƒ values() { [native code] }
console.log(obj[Symbol.iterator]) // undefined 未实现 iterator 接口// 调用迭代器工厂函数生成迭代器
const setIterator = set[Symbol.iterator]()
console.log(setIterator) // [Set Iterator] { 1, 2, 3 }// 后台自动调用迭代器函数.next()方法进行消费
for (const setElement of set) {
  console.log(setElement) // 1 2 3
}
​
// 因为实现 iterator 接口,无法生成迭代器导致报错
for (const objElement of obj) {
  console.log(objElement) // TypeError: obj is not iterable
}

迭代器协议

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

①IteratorResult包含两个属性:done和value 。done是一个布尔值,false表示还可以通过 next() 取得下一个值(value),true表示状态耗尽,value为undefined;②每个迭代器都是一次性对象,当done为true时后续调用next()一直返回同样的值 { done: true, value: undefined } 。③通过调用同一个迭代器工厂函数生成的不同迭代器实例相互之间没有联系,独立遍历可迭代对象;④可迭代对象通过工厂函数返回迭代器时,迭代器内部可迭代对象的值并不是固定的,而是可以动态改变,并且迭代器也会反应出来。

// 可迭代对象
const set = new Set().add(1).add(2).add(3)
const arr = [4, 5, 6]// 迭代器工厂函数
console.log(set[Symbol.iterator]) // ƒ values() { [native code] }
console.log(arr[Symbol.iterator]) // ƒ values() { [native code] }// 迭代器
const setIterator1 = set[Symbol.iterator]()
const setIterator2 = set[Symbol.iterator]()
const arrIterator = arr[Symbol.iterator]()
console.log(setIterator1) // SetIterator {1, 2, 3}
console.log(arrIterator) // Array Iterator {}// 执行迭代 ①IteratorResult两个属性值变化
console.log(setIterator1.next()) // { value: 1, done: false }
console.log(setIterator1.next()) // { value: 2, done: false }// ③ 每个迭代器实例独立循环可迭代对象,不存在第一个迭代器消费完,第二个也同步消费完情况
console.log(setIterator2.next()) // { value: 1, done: false }
console.log(setIterator1.next()) // { value: 3, done: false }//② 每个迭代器都是一次性对象,当可迭代对象值循环完毕会一直返回同样的值
console.log(setIterator1.next()) // { value: undefined, done: true }
console.log(setIterator1.next()) // { value: undefined, done: true }
console.log(setIterator1.next()) // { value: undefined, done: true }
​
console.log(arrIterator.next()) // { value: 4, done: false }
// ④ 改变原数组元素,下次迭代应该返回 5,因为动态增加了一个元素所以迭代器也相应的返回增加的那个元素 88
arr.splice(1, 0, 88)
console.log(arrIterator.next()) // { value: 88, done: false }
console.log(arrIterator.next()) // { value: 5, done: false }
console.log(arrIterator.next()) // { value: 6, done: false }
console.log(arrIterator.next()) // { value: undefined, done: true }

自定义迭代器

与可迭代对象类似,任何实现了Iterator接口的对象都可以作为迭代器使用。

class CustomIterable {
​
  limit = 0
​
  constructor(limit) {
    this.limit = limit
  }
​
  [Symbol.iterator]() {
    let count = 1
    const limit = this.limit
​
    return {
      next() {
        if (count <= limit) {
          return {
            done: false,
            value: count++,
          }
        } else {
          return {
            done: true,
            value: undefined,
          }
        }
      },
    }
  }
}
​
const customIterable = new CustomIterable(3)
​
const customIterableIterator = customIterable[Symbol.iterator]()
console.log(customIterableIterator.next()) // { done: false, value: 1 }
console.log(customIterableIterator.next()) // { done: false, value: 2 }
console.log(customIterableIterator.next()) // { done: false, value: 3 }
console.log(customIterableIterator.next()) // { done: true, value: undefined }
console.log(customIterableIterator.next()) // { done: true, value: undefined }// 特性会后台自动调用可迭代对象提供的工厂函数,创建一个迭代器
for (const customIterableElement of customIterable) {
  console.log(customIterableElement) // 1 2 3
}
​
for (const customIterableElement of customIterable) {
  console.log(customIterableElement) // 1 2 3
}
​
// 特性会后台自动调用可迭代对象提供的工厂函数,创建一个迭代器
console.log(...customIterable) // 1 2 3
console.log([...customIterable, 4, 5, 6]) // [ 1, 2, 3, 4, 5, 6 ]

以工厂函数生成的迭代器本身也实现了Iterable接口,[Symbol.iterator] 属性所引用的工厂函数会返回相同的迭代器

const arr = ['foo', 'bar', 'baz']
​
const iter1 = arr[Symbol.iterator]()
console.log(iter1) // Object [Array Iterator] {}
const iter2 = iter1[Symbol.iterator]()
console.log(iter1[Symbol.iterator]() === iter1) // true
console.log(iter1.next()) // { value: 'foo', done: false }
console.log(iter2.next()) // { value: 'bar', done: false }
console.log(iter1.next()) // { value: 'baz', done: false }
console.log(iter2.next()) // { value: undefined, done: true }const arrNumber = [1, 3, 5]
const iter = arrNumber[Symbol.iterator]()
​
for (const number of arrNumber) {
  console.log(number) // 1 3 5
}
​
for (const number of iter) {
  console.log(number) // 1 3 5
}
​

提前终止迭代器

通过可选的 return() 方法可以在迭代器消费完毕可迭代对象之前就“关闭”迭代器,包括:

  • for-of循环的 breakcontinuereturnthrow
  • 解构操作没有解构全部值

return()方法必须返回一个有效的 IteratorResult 对象,通常只返回 { done: true }

如果一个迭代器没有关闭方法那边下次迭代还会继续上传关闭的地方执行迭代器;return()方法并不是强制关闭的,某些迭代器执行了退出方法下次迭代还是会从上个地方继续;

class CustomIterable {
​
  limit = 0
​
  constructor(limit) {
    this.limit = limit
  }
​
  [Symbol.iterator]() {
    let count = 1
    const limit = this.limit
​
    return {
      next() {
        if (count <= limit) {
          return {
            done: false,
            value: count++,
          }
        } else {
          return {
            done: true,
            value: undefined,
          }
        }
      },
      return() {
        console.log('执行了退出迭代器方法')
        return {
          done: true,
        }
      },
    }
  }
}
​
const customIterable = new CustomIterable(5)
// console.log(customIterableIterator.next())
for (const customIterableIteratorElement of customIterable) {
  console.log(customIterableIteratorElement) // 1 2 3
  if (customIterableIteratorElement >= 3) break
  // 执行了退出迭代器方法
}
​
const arr = [1, 2, 3, 4, 5]
const arrIterator = arr[Symbol.iterator]()
arrIterator.return = function () {
  console.log('执行了退出迭代器方法')
  return {
    done: true
  }
}
​
for (const number of arrIterator) {
  console.log(number) // 1 2 3
  if (number >= 3) break
  // 执行了退出迭代器方法
}
​
for (const number of arrIterator) {
  console.log(number) // 4 5
}
​

生成器

生成器是ECMAScript 6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。

——《JavaScript高级程序设计(第四版)》

生成器基础

  • 生成器是一个函数,函数名称前面加一个* 就表示是生成器函数,只要是可以定义函数的地方就可以定义生成器(箭头函数除外);

    // 函数声明方式
    function * generator() { }
    // 函数表达式方式
    const generatorFn = function * () {}
    // 对象方法形式
    const foo = {
      * generatorFn() {}
    }
    // 类实例方法形式
    class Foo {
      * generatorFn() {}
    }
    // 类静态方法形式
    class Bar {
      static * generatorFn() {}
    }
    // 箭头函数形式报语法错误
    const generatorArrowFn = * () => {}
    
  • 调用生成器函数会产生一个生成器对象 ,其行为与迭代器类似,也实现了 Iterator 接口,因此也具有 next() 方法,调用next方法会返回IteratorResult对象 。生成器默认为暂停执行状态,调用一次生成器对象 开始或恢复 执行;

    function * generatorFn() {
      console.log('开始执行')
      yield 'foo'
    }
    // 默认为暂停状态并没有执行函数而是返回一个生成器对象
    const generatorObj = generatorFn()
    console.log(generatorObj) // Object [Generator] {}
    console.log('----------------') // ----------------
    // 调用第一次next()方法之后开始执行
    console.log(generatorObj.next())
    // 开始执行
    // { value: 'foo', done: false }
    console.log(generatorObj.next())
    // { value: undefined, done: true }
    
  • 当生成器对象执行遇到 return 或者不再有 yield 关键字时候,next方法返回值,done 属于值为 true ,否则为 falsevalue 属性值为 yield 关键字跟着值,或者函数 return 返回的值;

    function * generatorFn() {
      yield 'foo'
      return 'bar'
      // 不会执行到
      yield 'zzz'
    }
    ​
    const generatorObj = generatorFn()
    console.log(generatorObj.next()) // { value: 'foo', done: false }
    console.log(generatorObj.next()) // { value: 'bar', done: true }
    console.log(generatorObj.next()) // { value: undefined, done: true }
    console.log(generatorObj.next()) // { value: undefined, done: true }
    
  • 生成器函数内部执行和迭代器类似,区分作用域。同一个生成器函数产生不同生成器对象,各自执行next方法不会影响其他生成器对象;

    function * generatorFn() {
      yield 'foo'
      yield 'bar'
      yield 'baz'
    }
    ​
    const generatorObj1 = generatorFn()
    const generatorObj2 = generatorFn()
    console.log(generatorObj1.next()) // { value: 'foo', done: false }
    console.log(generatorObj1.next()) // { value: 'bar', done: false }
    console.log(generatorObj2.next()) // { value: 'foo', done: false }
    console.log(generatorObj1.next()) // { value: 'baz', done: false }
    console.log(generatorObj2.next()) // { value: 'bar', done: false }
    console.log(generatorObj2.next()) // { value: 'baz', done: false }
    console.log(generatorObj1.next()) // { value: undefined, done: true }
    console.log(generatorObj2.next()) // { value: undefined, done: true }
    
  • yield 关键字只能在生成器 函数作用域内使用 ,用在其他地方 函数作用域 内会抛出错误,在生成器函数嵌套函数内使用也会报错。

    // 正常
    function * generatorFn() {
      yield 'foo'
    }
    // 报错
    function * invalidGeneratorFnA () {
      function a() {
        yield 'foo'
      }
    }
    // 报错
    function * invalidGeneratorFnB() {
      const b = () => {
        yield 'foo'
      }
    }
    // 报错
    function * invalidGeneratorFnC() {
      (() => {
        yield 'foo'
      })()
    }
    
  • yield关键字不仅可以作为输出使用还可以作为 输入 。可以通过 next(value) 方法传参,对应的yield 来接收参数。

    function * generatorFn() {
      yield 'foo'
      return yield 'bar'
    }
    ​
    const generatorObj = generatorFn()
    console.log(generatorObj.next()) // { value: 'foo', done: false }
    // 因为returnyield都需要调用一次next()才会执行,所以这一次next执行的是yield 'bar' 这个语句
    // 再调用一次next并且传入参数会执行return
    console.log(generatorObj.next()) // { value: 'bar', done: false }
    console.log(generatorObj.next('baz')) // { value: 'baz', done: true }
    console.log(generatorObj.next()) // { value: undefined, done: true }
    

生成器对象作为可迭代对象

在生成器对象上显示调用 next() 方法用处不是很大,毕竟不是面向结果编程,初始并不知道有多少个,但是将生成器对象当成可迭代对象来使用,那将会十分方便,不仅函数可以当成可迭代对象来使用,星号(*) 还可以增加 yield 关键字,让它产生一个可迭代对象,从而使语法十分简洁。

function * generatorFn() {
  yield 'foo'
  yield 'bar'
  yield 'baz'
}
​
for (const generatorObj of generatorFn()) {
  console.log(generatorObj)
    // foo
    // bar
    // baz
}
​
function * generatorFnA() {
  yield * ['foo', 'bar', 'baz']
}
​
for (const generatorObj of generatorFnA()) {
  console.log(generatorObj)
  // foo
  // bar
  // baz
}

实际上 yield * [1, 2, 3] 将一个可迭代对象序列化为一连串可以单独产出的值,这和把yield放到循环里面一样。

// 这两个函数行为是等价的
function* generatorFnA() {
    for (const x of [1, 2, 3]) {
        yield x;
    }
}
​
for (const x of generatorFnA()) {
    console.log(x);
    // 1
    // 2
    // 3
}
function* generatorFnB() {
    yield* [1, 2, 3];
}
​
for (const x of generatorFnB()) {
    console.log(x);
    // 1
    // 2
    // 3
}

因为生成器函数调用后产生迭代器,所以生成器函数适合作为默认迭代器

const obj = {
    values: [1, 2, 3, 'foo'],
    * [Symbol.iterator]() {
        yield* this.values
    }
}
​
for (const objElement of obj) {
    console.log(objElement)
    // 1
    // 2
    // 3
    // foo
}

这里 for-of 循环调用了对象的默认迭代器,调用默认迭代器产生了一个生成器对象,因为生成器对象是可迭代的,所以可以被for-of进行迭代。

生成器函数实现递归算法

function* recursive(n) {
    if (n > 0) {
        yield * recursive(n - 1);
        yield n - 1;
    }
}
​
for (const x of recursive(5)) {
    console.log(x);
    // 0
    // 1
    // 2
    // 3
    // 4
}

提前终止生成器

与迭代器类似,生成器也可以提前终止。

  • return() 会强制进入关闭状态,返回的值就是return提供的值,一旦调用了return则无法恢复可执行状态了。

    function * generatorFn() {
        yield* [1, 2, 3];
    }
    const generatorObj = generatorFn();
    console.log(generatorObj.next()) // { value: 1, done: false }
    console.log(generatorObj.return(4)) // { value: 4, done: true }
    console.log(generatorObj.next()) // { value: undefined, done: true }
    console.log(generatorObj.next()) // { value: undefined, done: true }const generatorObj1 = generatorFn();
    ​
    // for循环会忽略状态done为true的值,所以return(5)什么都没有返回
    for (const number of generatorObj1) {
        console.log(number)
        if (number > 1) {
            generatorObj1.return(5)
        }
    }
    // 1
    // 2
    
  • throw() 会将一个错误注入到生成器对象中,如果错误没有处理那么生成器就会关闭,否则会跳过对应的yield并且恢复执行。

    function* generatorFn() {
        yield* [1, 2, 3];
    }
    ​
    const generatorObj = generatorFn();
    try {
        generatorObj.throw('foo');
    } catch (e) {
        console.error(e); // foo
    }
    console.log(generatorObj.next()); // { value: undefined, done: true }console.log('---------------')
    ​
    function * generatorFnA() {
        for (const x of [1, 2, 3]) {
            try {
                yield x;
            } catch(e) {}
        }
    }
    const generatorObj1 = generatorFnA();
    console.log(generatorObj1.next()); // { value: 1, done: false }
    generatorObj1.throw('foo');
    console.log(generatorObj1.next()); // { value: 3, done: false }