《JavaScript 高级程序设计》第七章 迭代器与生成器 学习记录

223 阅读12分钟
  • 迭代 iteration , "重复","再来"。
  • 按顺序反复多次执行一段程序。

1、理解迭代

  • 数组可以通过递增索引遍历,但并不理想
    • 迭代之前要事先知道如何使用数据结构
    • 遍历顺序并不是数据结构固有的
  • ES5新增Array.prototype.forEach()
    • 解决了单独计量索引和通过数组对象取得值问题
    • 但没办法标识迭代何时终止,只适用于数组。
  • 迭代器模式:开发者无须实现知道如何迭代就能实现迭代操作,ES6后开始支持。

2、迭代器模式

  • 迭代器模式描述一个方案,即可以把有些结构称为“可迭代对象(iterable)”,因为他们实现了正式的Iterabel接口,而且可以通过迭代器Iterator消费。
  • 可迭代对象可理解为 数组或集合这样的集合类型的对象。包含元素有限,都具有无歧义的遍历顺序。
  • 可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,该循环中生成的值是暂时性的,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。
  • 临时性可迭代对象可以实现为生成器。
  • 任何实现Iterable接口的数据结构都能被实现Iterator接口的结构“消费”。迭代器是按需创建的一次性对象。每个迭代器会关联一个可迭代对象,而迭代器会暴露迭代器关联可迭代对象的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。

1、可迭代协议

  • 实现Iterable接口s必须具备两种能力

    • 支持迭代的自我识别功能
    • 创建实现Iterator接口的对象的能力
  • 必须暴露一个属性作为“默认迭代器”

  • 这个属性必须使用特殊的Symbol.iterator作为键

  • 这个默认迭代器属性必须引用一个迭代器工厂函数

  • 调用这个工厂函数必须返回一个新迭代器。

  • 很多内置类型都实现了Iterable接口,检查是否存在默认迭代器属性可以暴露这个工厂函数

    let num = 1
    let obj = {}
    
    // 没内置
    num[Symbol.iterator] // undefined
    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');
    let els = document.querySelectorAll('div');
    // 这些类型都实现了迭代器工厂函数
    // f values() { [native code] }
    str[Symbol.iterator] 
    arr[Symbol.iterator] 
    map[Symbol.iterator]
    set[Symbol.iterator] 
    els[Symbol.iterator] 
    
    // 调用这个工厂函数会生成一个迭代器
    str[Symbol.iterator](); // StringIterator {}
    arr[Symbol.iterator](); // ArrayIterator {}
    map[Symbol.iterator](); // MapIterator {}
    set[Symbol.iterator](); // SetIterator {}
    els[Symbol.iterator](); // ArrayIterator {}
    
  • 实际不需要显式调用这个工厂函数生成迭代器,可接收迭代对象的原生语言特征:

    • for-of 循环
    • 数组解构
    • 扩展操作符
    • Array.from()
    • 创建集合
    • 创建映射
    • Promise.all() 接收有期约组成的可迭代对象
    • Promise.race() 接收有期约组成的可迭代对象
    • yield* 操作符,在生成器中使用
  • 如果对象原型链上的父类实现了Iterable接口,可继承

    class FooArray extends Array {}
    let fooArr = new FooArray('foo','bar','baz')
    for(let el of fooArr){
      ...
    }
    

2、迭代器协议

  • 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。

  • next() 在迭代对象中遍历数据

  • 每次调用next() 都会返回一个IteratorResult对象

    • 包含迭代器返回的下一个值
  • 如果不调用next() 则无法知道迭代器当前位置

  • IteratorResult对象包含两个属性

    • done 布尔值 表示是否还可以调用next() 取得下一个值
    • value包含可迭代对象的下一个值(done为false),或者undefined(done为true)
    // 可迭代对象
    let arr = ['foo','bar']
    
    // 迭代器工厂函数
    arr[Symbol.iterator]//f values(){[native code]}
    
    // 迭代器
    let iter = arr[Symbol.iterator]()
    iter // ArrayIterator()
    
    //执行迭代
    iter.next() // {done: false, value: 'foo'}
    iter.next() // {done: false, value: 'bar'}
    iter.next() // {done: true, value: undefined}
    
  • 只要迭代器达到done: true 状态,后续调用next() 都一直返回相同的值了。

  • 每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器实例之间没有联系。

  • 迭代器不与某个时刻的快照绑定,仅仅是记录遍历可迭代对象的历程,如果期间修改了,迭代器也会变化

    let arr = [1,2]
    let iter = arr[Symbol.iteator]()
    iter.next() // {done: false, value: 1}
    
    arr.splice(1,0,0)
    iter.next() // {done: false, value: 0}
    iter.next() // {done: false, value: 2}
    iter.next() // {done: true, value: undefined}
    
  • 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。

  • “迭代器”,可以指通用的迭代,也可指借口,可以指正式的迭代器类型。

    // 这个类实现了可迭代接口(Iterable)
    // 调用默认的迭代器工厂函数会返回
    // 一个实现迭代器接口(Iterator)的迭代器对象
    class Foo {
      [Symbol.iterator]() {
      return {
       next() {
        return {done: false, value: "foo"}
          }
        }
      }
    }
    
    let f = new Foo() 
    f[Symbol.iteator]() // { next: f() {} }
    
    // Array类型实现了可迭代接口(Iterable)
    // 调用Array类型的默认迭代器工厂函数
    // 会创建一个ArrayIterator实例
    let a = new Array()
    a[Symbol.iterator]() // Array Iterator{}
    

    3、自定义迭代器

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

    // 每个实例只能迭代一次
    class Counter {
     constructor(limit) {
      this.count = 1
        this.limit = limit
      }
      
      next() {
        if(this.count <= this.limit) {
          return {done: false, value: this.count++}
        }else {
       return {done: true, value: undefined}
        }
      }
      
      [Symbol.iterator]() {
        return this
      }
    }
    let counter = new Counter(3)
    for(let i of counter) {
      ...
    }
    
  • 为了让一个可迭代对象创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。

    class Counter {
      constructor(limit){
        this.limit = limit
      }
      [Symbol.iterator]() {
        let count = 1;
        let limit = this.limit;
        return {
          next() {
            if(count <= limit) {
              return {done: false, value: count++}
            }else {
         return {done: true, value: undefined}
            }
          }
        }
      }
    }
    let counter = new Counter(3)
    for(let i of counter) {
      ...
    }
    for(let i of counter) {
      ...
    }
    
  • 每个以这种方式创建的迭代器也实现了Iterable接口。

  • Symbol.iterator 属性引用的工厂函数会返回相同的迭代器

    let arr = [1,2,3]
    let iter1 = arr[Symbol.iterator]()
    let iter2 = iter1[Symbol.iterator]()
    iter1 === iter2
    
  • 因为每个迭代器实现了Iterable接口,所以可以用在任何期待可迭代对象的地方。

4、提前终止迭代器

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

  • 执行迭代的结构在想让迭代器知道他不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。

    • for-of 通过break,continue,return,throw提前退出
    • 解构操作并未消费所有值
  • return() 方法必须返回一个有效的IteratorResult对象。

    class Counter{
      constructor(limit) {
        this.limit = limit
      }
      
      [Symbol.iterator]() {
        let count = 1;
        let limit = this.limit;
        return {
          next() {
            if(count <= limit) {
              return {done: false, value: count++}
            }else {
         return {done: true}
            }
          },
          return() {
            console.log('Exiting early')
            return {done: true}
          }
        }
      }
    }
    
    let counter1 = new Counter(5)
    
    for(let i of counter1) {
      if(i > 2) {
        break
      }
      console.log(i)
    }
    
    let counter2 = new Counter(5)
    try{
      for(let i of counter2) {
        if(i > 2) {
          throw 'err'
        }
      console.log(i)
      }
    }catch(e) {}
    
    let counter3 = new Counter(5)
    let [a, b] = counter3
    
    
  • 若迭代器没有关闭,则可以继续从上次离开的地方迭代

  • 因为return() 方法可选,所以并非所有迭代器都是可以关闭的。可以测试return属性是不是函数对象。不过仅仅给不可关闭的迭代器增加这个方法不能让它变成可关闭的,因为调用return() 不会强制迭代器进入关闭状态。但return还是会调用

    let a = [1,2,3,4,5]
    let iter = a[Symbol.iterator]()
    iter.return = function () {
      console.log('Exiting early')
      return {done: true}
    }
    
    for(let i of iter) {
      console.log(i)
      if(i > 2){
        break;
      }
    }
    // 1
    // 2
    // 3
    // 'Exiting early'
    
    for(let i of iter) {
      console.log(i)
    }
    // 4
    // 5
    

    3、生成器

  • 生成器拥有在一个函数块内暂停和恢复代码执行的能力。

  • 这中能里可以自定义迭代器和实现协程

1、生成器基础

  • 生成器的形式是一个函数,函数名前面一个星号(*)表示他是一个生成器。

  • 可以定义函数的地方,都可以定义生成器

    // 生成器函数声明
    function* generatorFn() {}
    
    // 生成器函数表达式
    let generatorFn = function* () {}
    
    // 作为对象字面量方法的生成器函数
    let foo = {
      * generationFn() {}
    }
    
    // 作为类实例方法的生成器函数
    class Foo {
      * generationFn() {}
    }
    
    // 作为静态方法的生成器函数
    class Foo {
      static * generationFn() {}
    }
    
    • 箭头函数不能用来定义生成器函数
  • 标识生成器函数的星号不受两侧空格影响

    // 等价的
    function* generatorFnA() {}
    function *generatorFnB() {}
    function * generatorFnC() {}
    
    class Foo {
     *generatorFnD() {}
      * genreatorFnF() {}
    }
    
  • 调用生成器函数会产生一个生成器对象

    • 一开始处于暂停状态

    • 与迭代器相似,也实现了Iterator接口,有next()方法

    • 调用这个方法会让生成器开始或恢复执行

      function* generatorFn() {}
      const g = generatorFn()
      g // generatorFn {<suspended>}
      g.next // f next() { [native code] }
      
    • next()方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。

    • 函数体为空的生成器函数中间不会停留,调用一次 next()就会让生成器到达 done: true 状态。

      function* generatorFn() {}
      let generatorObject = generatorFn();
      generatorObject; // generatorFn {<suspended>}
      generatorObject.next(); // { done: true, value: undefined }
      
    • value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定

      function* generatorFn() {
       return 'foo';
      }
      let generatorObject = generatorFn();
      generatorObject; // generatorFn {<suspended>}
      generatorObject.next(); // { done: true, value: 'foo' }
      
    • 生成器函数只会在初次调用 next()方法后开始执行

      function* generatorFn() {
       console.log('foobar');
      }
      // 初次调用生成器函数并不会打印日志
      let generatorObject = generatorFn();
      generatorObject.next(); // foobar
      
    • 生成器对象实现了 Iterable 接口,它们默认的迭代器是自引用的

      function* generatorFn() {}
      
      generatorFn;
      // f* generatorFn() {}
      
      generatorFn()[Symbol.iterator]; 
      // f [Symbol.iterator]() {native code}
      
      generatorFn();
      // generatorFn {<suspended>}
      
      generatorFn()[Symbol.iterator]();
      // generatorFn {<suspended>}
      
      const g = generatorFn();
      g === g[Symbol.iterator]();
      // true 
      

2、通过yield中断执行

  • yield可以让生成器停止和开始执行。

  • 生成器函数在遇到yield关键字之前会正常执行。

  • 遇到这个关键字的时执行会停止,函数作用域的状态保留。

  • 停止执行的生成器函数只能通过在生成器对象上调用next() 方法恢复执行。

    function* generatorFn() {
      yield;
    }
    let generatorObject = generatorFn();
    generatorObject.next() //{done: false, value: undefined}
    generatorObject.next() //{done: true, value: undefined}
    
  • yield像函数的中间返回语句

  • 生成的值会出现在next()方法返回的对象中

  • 通过yield关键字推出的生成器函数会处在done: false状态

  • 通过return 退出的会处于 done : true 状态

    function* generatorFn() {
     yield 'foo';
     yield 'bar';
     return 'baz';
    }
    let generatorObject = generatorFn();
    console.log(generatorObject.next()); 
    // { done: false, value: 'foo' }
    console.log(generatorObject.next());
    // { done: false, value: 'bar' }
    console.log(generatorObject.next()); 
    // { done: true, value: 'baz' } 
    
  • 内部执行流程会针对每个生成器对象区分作用域,不会互相影响。

  • yield只能在生成器函数内部使用,在其他地方会抛错,必须直接定位于生成器函数定义中,出现在嵌套的非生成器函数中会报错。

    // 有效
    function* validGeneratorFn() {
     yield;
    }
    // 无效
    function* invalidGeneratorFnA() {
     function a() {
      yield;
     }
    }
    // 无效
    function* invalidGeneratorFnB() {
     const b = () => {
      yield;
     }
    }
    // 无效
    function* invalidGeneratorFnC() {
     (() => {
      yield;
     })();
    } 
    

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

  • 显式调用next() 用处不大,当成可迭代对象更方便。

    function* generatorFn() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    for(const x of generatorFn()) {
      console.log(x)
    }
    // 1
    // 2
    // 3
    
  • 执行指定次数

    function* nTimes(n) {
     while(n--){
        yield
      }
    }
    for(let _ of nTimes(2)) {
      console.lgo('foo')
    }
    // foo
    // foo
    

2、使用yield实现输入输出

  • 作为函数中间参数使用,上一次让生成器暂停的yield关键字会接收到next() 方法的第一个值。

  • 第一次调用的next()传入的值不会使用,因为第一次是为了开始执行生成器函数。

    function* generatorFn(initial) {
      console.log(initial)
      console.log(yield)
      console.log(yield)
    }
    
    let generatorObject = generatorFn('foo')
    
    generatorObject.next('bar') // foo
    generatorObject.next('baz') // baz
    generatorObject.next('qux') // qux
    
  • yield 可以同时用于输入输出

    function* generatorFn() {
     return yield 'foo'
    }
    let generatorObject = generatorFn()
    generatorObject.next() 
    //{done: false, value: 'foo'}
    generatorObject.next('bar')
    //{done: true, value: 'bar'}
    
  • 函数必须要对整个表达式求值才能确定要返回的值,在遇到yield关键字时暂停执行并计算出要产生的值foo,下一次调研next()传入了bar,作为交给同一个yield的值。然后被确定为本次生成器函数要返回的值。

  • yield并非只能用一次

    function* genreatorFn() {
     for(let i = 0; ++i){
      yield i;
      }
    }
    
  • 根据配置的值迭代相应次数产生迭代的索引。

    function* nTimes(n) {
      for(let i = 0; i < n; i++) {
        yield i
      }
    }
    
    // while
    function* nTimes(n) {
      let i = 0
      while(n--) {
        yield i++
      }
    }
    
    for(let x of nTimes(3)){
      console.log(x)
    }
    
  • 可以用来实现范围和填充数组

    function* range(start, end) {
      while(end > start) {
        yield start++
      }
    }
    for(const x of range(4,7)){
      console.log(x)
    }
    // 4
    // 5
    // 6
    
    function* zeroes(n) {
      while(n--) {
        yield 0
      }
    }
    Array.from(zeroes(8))
    //[0,0,0,0,0,0,0,0]
    

3、产生可迭代对象

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

    function* generatorFn() {
      for(const x of [1,2,3]){
        yield x
      }
    }
    
    function* generatorFn() {
      yield* [1,2,3]
    }
    
  • yield 星号两侧空格不影响

    function* generatorFn() {
     yield* [1,2]
     yield *[3,4]
      yield * [5,6]
    }
    for(const x of generatorFn()) {
      console.log(x)
    }
    //1-6
    
    • yield* 实际上只是讲一个可迭代对象序列化为一连串可以单独产出的值。
  • yield*的值是关联迭代器返回done: true时value的属性,

    • 普通迭代器来说是undefined
    function* generatorFn() {
     console.log('iter value:',yield* [1,2,3])
    }
    for(const x of generatorFn()) {
      console.log('value': x)
    }
    // value: 1
    // value: 2
    // value: 3
    // iter value: undefined
    
    • 对于生成器函数,这个值是生成器函数返回的值
    function* innerGeneratorFn() {
     yield 'foo'
      return 'bar'
    }
    
    function* outerGeneratorFn() {
      console.log('iter value:',yield* innerGeneratorFn())
    }
    for(const x of outerGeneratorFn()) {
      console.log('value': x)
    }
    // value: foo
    // iter value: bar
    

4、使用yield*实现递归算法

  • 最有用的地方是实现递归操作,生成器产生自身

    function* nTimes(n) {
      if(n > 0){
        yield* nTimes(n - 1)
        yield n-1
      }
    }
    for(const x of nTimes(3)){
      console.log(x)
    }
    // 0
    // 1
    // 2
    
  • 图的实现

    class Node {
      constructor(id) {
        this.id = id
        this.neighbors = new Set()
      }
      connect(node) {
        if(node !== this) {
          this.neighbors.add(node)
          node.neighbors.add(this)
        }
      }
    }
    
    class RandomGraph {
      constructor(size) {
        this.nodes = new Set()
        
        // 创建节点
        for(let i = 0; i < size; i++) {
          this.nodes.add(new Node(i))
        }
        
        // 随机连接节点
        const threshold = 1 / size
        for(const x of this.nodes) {
          for(const y of this.nodes) {
            if(Math.random() < threshold) {
              x.connect(y)
            }
          }
        }
      }
      
      print() {
        for(const node of this.nodes) {
          const ids = [...node.neighbors]
                 .map(n=>n.id)
                 .join(',');
          console.log(`${node.id}:${ids}`)
        }
      }
    }
    
    const g = new RandomGraph(6)
    g.print
    
    • 图的结构适合递归遍历,生成器函数必须接收一个可迭代对象,产出该对象中的每一个值,并且对每个值进行递归,可以用来测试某个图是否连通。是否没有不可到达的节点(深度优先遍历)
    class Node {
      constructor(id) {
        ...
      }
      connect(node) {
       ...
      }
    }
    
    class RandomGraph {
      constructor(size) {
       ...
      }
      
      print() {
       ...
      }
       
      isConnected() {
        const visitedNodes = new Set()
        
        function* traverse(nodes){
       for(const node of nodes){
            if(!visitedNodes.has(node)){
              yield node;
              yield* traverse(node.neighbors)
            }
          }
        }
        
        // 取得集合中第一个节点
        const firstNode = this.nodes[Symbol.iterator]().next().value
        
        // 使用递归生成器迭代每个节点
        for(const node of traverse([firstNode])){
          visitedNodes.add(node)
        }
        return visitedNodes.size === this.nodes.size
      }
    }
    

    3、生成器作为默认迭代器

class Foo {
 constructor() {
    this.value = [1,2,3]
  }
  * [Symbol.iterator]() {
    yield* this.value
  }
}

const f = new Foo()
for(const x of f){
  console.log(x)
}
  • for-of循环调用了默认迭代器(也是生成器函数)并产生了一个生成器对象,这个生成器对象是可迭代的,所以完全可以在迭代中使用。

4、提前终止生成器

  • 生成器除了包含next() ,return()外还有一个throw()

    function* generatorFn() {}
    const g = generatorFn();
    
    g // generatorFn {<suspended>}
    g.next // f next() { [native code] }
    g.return // f return() { [native code] }
    g.throw // f throw() { [native code] }
    
  • return()和 throw()方法都可以用于强制生成器进入关闭状态。

1、return()

  • 强制生成器进入关闭状态,提供给return() 方法的值,就是终止迭代器对象的值

    function* generatorFn() {
      for(const x of [1,2,3]) {
        yield x;
      }
    }
    
    const g = generatorFn()
    g // generatorFn {<suspended>}
    g.return(4) // {done: true, value: 4}
    g // generatorFn {<closed>}
    
  • 所有生成器对象都有return方法,只要通过它进入关闭状态,就无法恢复了。后续再调用next() 都会返回done: true状态

  • for-of循环等内置语言会自动忽略状态为done: true 的InteratorObject 内部返回的值。

    function* generatorFn() {
      for(const x of [1,2,3]) {
        yield x;
      }
    }
    
    const g = generatorFn()
    for(const x of g) {
      if(x > 1) {
        g.return(4)
      }
      console.log(x)
    }
    //1
    //2
    

2、throw()

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

    function* generatorFn() {
      for(const x of [1,2,3]) {
        yield x
      }
    }
    
    const g = generatorFn()
    g // generatorFn {<suspended>}
    try{
      g.throw('foo')
    }catch(e) {
      console.log(e) // foo
    }
    g // generatorFn {<closed>}
    
  • 如果生成器函数内部处理了这个错误,生成器就不会被关闭,可以恢复执行,错误处理会跳过对应的yield。

    function* generatorFn() {
      for(const x of [1,2,3]) {
        try{
          yield x
        }catch(e) {}
      }
    }
    
    const g = generatorFn()
    g.next() // {done: false, value: 1}
    g.throw('foo') // 实际返回 {done: false, vlaue: 2}
    g.next() // {done: false, value: 3}
    
  • 如果生成器对象还没有开始执行,那么调用throw() 抛出的错误不会在函数内部被捕获,因为相当于在函数块外部抛出了错误