七、迭代器与生成器

迭代器

迭代器模式

在ECMAScript语境下,把有些结构称为“可迭代对象”(Iterable),因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费

实现Iterable接口需要同时具备两种能力:

  • 支持迭代的自我识别能力
  • 创建实现Iterator接口的对象的能力

因此可迭代对象需要暴露一个属性作为“默认迭代器”,且这个属性的键必须为"Symbol.iterator";

这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器

let arr = ['a', 'b']
// 这是数组的迭代器工厂函数
arr[Symbol.iterator]
// 调用这个工厂函数会生成一个迭代器
arr[Symbol.iterator]() // ArrayIterator {} 

在实际写代码过程中,不需要显式的调用这个工厂函数来生成迭代器;实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性;原生的包括,for-of、数组解构、扩展操作符、Promise.all等

迭代器协议

  • 迭代器API使用next() 方法在可迭代对象中遍历数据,每次调用成功返回一个IteratorResult对象。该对象包含两个属性valuedone 用于表示记录迭代进度

    let arr = ['foo', 'bar']
    let iter = arr[Symbol.Iterator]()
    console.log(iter.next()) // {done: false, value: 'foo'}
    console.log(iter.next()) // {done: false, value: 'bar'}
    console.log(iter.next()) // {done: true, value: undefined}
    
  • 迭代器对象仅记录迭代历程,中途对象改变,迭代进度不会被改变

    let arr = ['foo', 'baz']
    let iter = arr[Symbol.Iterator]()
    console.log(iter.next()) // {done: false, value: 'foo'}
    arr.splice(1, 0, 'bar')
    console.log(iter.next()) // {done: false, value: 'bar'}
    console.log(iter.next()) // {done: false, value: 'baz'}
    

自定义迭代器

一个简单的迭代器

class Counter {
  constructor(limit) {
    this.limit = limit
  }
  [Symbol.iterator]() {
    // 把计数器放到闭包里,然后通过闭包返回迭代器
    // 如果把计数器放到实例属性里,会出现每个实例只能迭代一次的问题
    let counter = 1,
        limit = this.limit;
    return {
      next() {
        if (count <= limit) {
          return { done: false, value: count++ }
        } else {
          return { done: true, value: undefined}
        }
      }
    }
  }
}

生成器

  • 生成器拥有在一个函数块内暂停和恢复代码执行的能力,通过在函数名前加星号*声明;只要能定义函数的地方就能定义生成器,但不能用箭头函数定义

    // 星号两侧的空格不影响定义
    function* fn() {}
    let fn2 = function * () {}
    let foo = {
      *fn3() {}
    }
    class Foo {
      * fn4() {}
    }
    
  • 生成器与迭代器类似,调用生成器函数会返回一个生成器对象,生成器对象一开始处于暂停执行(suspended) 的状态,需要调用next()方法开始或恢复执行(生成器也实现了Iterator接口)

    也就是说只有在调用next函数之后,生成器函数体内的代码才会执行

yield

  • yield关键字可以让生成器停止和开始,只能在生成器函数内部使用;代码执行到这个关键字时,执行会停止,函数作用域的状态会被保留;

  • yield可以用于输入或输出,也可以同时使用

  • 用于输出时,yield类似于return会返回值,不同在于yield是一种中间状态,返回的done状态为false

    function* generatorFn() {
      yield 'foo';
      return 'baz'
    }
    let generatorObj = generatorFn()
    console.log(generatorObj.next()) // { done: false, value: 'foo' }
    console.log(generatorObj.next()) // { done: true, value: 'baz' }
    // 相比于显式的调用next,更多的是把生成器对象当迭代对象使用
    let (const x of generatoFn()) {
      console.log(x)
    }
    // foo
    // baz
    
  • 用作输入时候,yield会接收传给next方法的第一个值;需要注意的是第一次调用next的值不会被传入,因为第一次调用next是为了启动生成器函数

    function* generatorFn(initial) {
      console.log(initial)
      console.log(yield)
      console.log(yield)
    }
    let generatorObj = generatorFn('foo')
    console.log(generatorObj.next('bar')) // { done: false, value: 'foo' }
    console.log(generatorObj.next('baz')) // { done: false, value: 'baz' } 
    
  • 可以使用星号增强yield,yield*

    • yield* 后 可以跟一个可迭代对象,一次产出一个值

      // 二者等价
      function* generatorFn1() {
        for (const x of [1, 2, 3]) {
          yield x
        }
      }
      function* generatorFn2() {
        yield* [1, 2, 3]
      }
      
  • yield* 非常适合实现递归算法

你可能感兴趣的:(七、迭代器与生成器)