夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 第1章 什么是JavaScript
  • 第2章 HTML中的JavaScript
  • 第3章 语言基础
  • 第4章 变量、作用域与内存
  • 第5章 基本引用类型
  • 第6章 集合引用类型
  • 第7章 迭代器与生成器
    • 第8章 对象、类与面向对象编程
    • 第9章 代理与反射
    • 第10章 函数
    • 第11章 异步编程
    • 第12章 BOM
    • 第14章 DOM
    • 第15章 DOM扩展
    • 第16章 DOM2和DOM3
    • 第17章 事件
    • 第18章 动画与Canvas图形
    • 第19章 表单脚本
    • 第20章 JavaScript API
    • 第21章 错误处理与调试
    • 第23章 JSON
    • 第24章 网络请求与远程资源
    • 第25章 客户端存储
    • 第26章 模块
    • 第27章 工作者进程
    • 第28章 最佳实践
    • 《JavaScript高级程序设计》
    frost
    2021-09-16

    第7章 迭代器与生成器

    # 迭代器与生成器

    迭代是按照顺序反复多次执行一段程序,通常有明确的终止条件。

    循环是迭代的基础,使用索引来进行遍历并不能满足所有数据结构的迭代,比如 Map,Set,因为他们可能没有索引。

    Array.prototype.forEach不需要索引了,但不能终止迭代,而且仅限于数组使用。

    # 迭代器模式

    迭代器模式规定了一些数据结构,成为“可迭代对象”,它们实现了正式的 Iterable 接口,并且可以通过迭代器 Iterator 消费。

    “可迭代对象”,比如数组,Map,Set 等有序集合

    # 可迭代协议

    默认迭代器属性:Symbol.iterator,它指向一个迭代器工厂函数,返回一个迭代器。

    已实现了 Iterator 接口的数据类型:

    • 字符串
    • 数组
    • 映射
    • 集合
    • arguments 对象
    • NodeList 等 DOM 集合类型
    let str = 'abc'
    let arr = ['a', 'b', 'c']
    let map = new Map().set('a', 1).set('b', 2)
    let set = new Set().add('a').add('b')
    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 ] }
    console.log(set[Symbol.iterator]()) // [Set Iterator] { 'a', 'b' }
    
    let noop = function() {
      console.log(arguments[Symbol.iterator]()) // Object [Array Iterator] {}
    }
    noop('a', 'b', 'c')
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    js 有原生语言特性来隐式调用这个工厂函数,从而生成迭代器,包括:

    • for-of 循环
    • 数组解构
    • 扩展操作符
    • Array.from()
    • 创建集合(Set)和映射(Map)
    • Promise.all()和 Promise.race()接收可迭代对象
    • yield*操作符
    let arr = [1, 2, 3]
    
    // for-of
    for (let item of arr) {
      console.log(item) // 1 2 3
    }
    
    // 数组解构
    let [a, b, c] = arr
    console.log(a, b, c) // 1 2 3
    
    // 扩展操作符
    let arr2 = [...arr]
    console.log(arr2) // [ 1, 2, 3 ]
    
    // Array.from()
    let arr3 = Array.from(arr)
    console.log(arr3) // [ 1, 2, 3 ]
    
    // Set
    let set = new Set(arr)
    console.log(set) // Set(3) { 1, 2, 3 }
    
    // Map
    let pairs = arr.map((x) => ['prefix' + x, x])
    let map = new Map(pairs)
    console.log(map) // Map(3) { 'prefix1' => 1, 'prefix2' => 2, 'prefix3' => 3 }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    # 迭代器协议

    迭代器通过迭代器协议,来迭代与其关联的可迭代对象。

    迭代器调用 next()方法来遍历数据,每次调用 next()都会返回 IteratorResult 对象,包含 2 个属性:done 和 value。

    done 为 true 时,表示遍历完成,此时 value 为 undefined;为 false 时,value 为可迭代对象中的当前遍历到的值。

    let arr = ['foo', 'bar']
    const iter = arr[Symbol.iterator]()
    
    console.log(iter.next()) // { value: 'foo', done: false }
    console.log(iter.next()) // { value: 'bar', done: false }
    console.log(iter.next()) // { value: undefined, done: true }
    console.log(iter.next()) // { value: undefined, done: true }
    
    1
    2
    3
    4
    5
    6
    7

    当 done 为 true 后,之后再调用就一直返回同样的值。

    不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。

    # 自定义迭代器

    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
      }
    }
    const counter = new Counter(3)
    for (let item of counter) {
      console.log(item) // 1 2 3
    }
    for (let item of counter) {
      console.log(item) // (nothing logged)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    上述代码实现了 next()和Symbol.iterator方法,即实现了 Iterator 接口。但每个实例只能迭代一次。下面利用闭包来存储计数器变量,从而可以创建多个迭代器。

    class Counter {
      constructor(limit) {
        this.limit = limit
      }
      [Symbol.iterator]() {
        let count = 1,
          limit = this.limit
        return {
          next() {
            if (count <= limit) {
              return { done: false, value: count++ }
            } else {
              return { done: true, value: undefined }
            }
          },
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # 提前终止迭代器

    可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。

    这句话有点拗口。意思是迭代器暴露一个和 next 同级的 return 方法,调用方如 for...of 结构可以在遍历未完成时,退出遍历。

    可能的情况有 2 类:

    • for...of 的 break、continue、return、throw 语句提前退出
    • 解构一部分值
    class Coutner {
      constructor(limit) {
        this.limit = limit
      }
      [Symbol.iterator]() {
        let limit = this.limit,
          count = 1
        return {
          next() {
            if (count <= limit) {
              return { done: false, value: count++ }
            } else {
              return { done: true, value: undefined }
            }
          },
          return() {
            return { done: true }
          },
        }
      }
    }
    const counter = new Coutner(3)
    for (let item of counter) {
      console.log(item) // 1 2 3
    }
    for (let item of counter) {
      if (item < 3) {
        console.log(item) // 1 2
      } else {
        break
      }
    }
    
    const [a, b] = counter
    console.log(a, b) // 1 2
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    # 生成器

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

    # 生成器基础

    只要是可以定义函数的地方,就可以定义生成器,通常写法为

    function* generatorFn() {}
    
    1

    调用生成器函数会产生一个生成器对象,生成器对象实现了 Iterator 接口,因此有 next()方法,返回一个对象,包含 done 和 value 属性。

    function* generatorFn() {
      console.log('start')
      return 'foo'
    }
    
    // 调用生成器函数不会执行里面的代码
    const g = generatorFn()
    console.log(g === g[Symbol.iterator]()) // true
    
    // 在初次调用next方法后,生成器里面的代码才执行
    console.log(g.next()) // start { value: 'foo', done: true }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # 通过 yield 中断执行

    yield 关键字可以让生成器停止和开始执行。具体是遇到 yield 后,执行停止,只能通过生成器对象调用 next 方法恢复执行。yield 有点像函数的中间返回语句,它生成的值会出现在 next()方法返回的对象里。

    function* generatorFn() {
      yield 'foo'
      yield 'bar'
      return 'baz'
    }
    const g = generatorFn()
    console.log(g.next()) // { value: 'foo', done: false }
    console.log(g.next()) // { value: 'bar', done: false }
    console.log(g.next()) // { value: 'baz', done: true }
    console.log(g.next()) // { value: undefined, done: true }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    yield 关键字只能直接在生成器函数内部使用,即必须配合*使用。

    1. 生成器对象作为可迭代对象使用

      function* generatorFn() {
        yield 1
        yield 2
        yield 3
      }
      for (let x of generatorFn()) {
        console.log(x) // 1 2 3
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
    2. 使用 yield 实现输入输出

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

      function* generatorFn(initial) {
        console.log('1', initial)
        console.log('2', yield)
        console.log('3', yield)
      }
      
      const g = generatorFn('foo')
      g.next('bar') // 1 foo
      g.next('baz') // 2 baz
      g.next('qux') // 3 qux
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      因此 yield 可以同时用于输入和输出。

      function* generatorFn() {
        return yield 'foo'
        // 上一行等同于
        // const a = yield 'foo'
        // console.log('a', a) // bar
        // return a
      }
      const g = generatorFn()
      console.log(g.next()) // { value: 'foo', done: false }
      console.log(g.next('bar')) // { value: 'bar', done: true }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
    3. 产生可迭代对象

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

      // function* generatorFn() {
      //   for (const x of [1, 2, 3]) {
      //     yield x
      //   }
      // }
      // 等同于
      function* generatorFn() {
        yield* [1, 2, 3]
      }
      for (let x of generatorFn()) {
        console.log(x) // 1 2 3
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      再来看一个例子

      function* generatorFn() {
        yield 'foo'
        yield 'baz'
        return 'bar'
      }
      
      function* outerGenerationFn() {
        console.log('iter value', yield* generatorFn()) // bar
      }
      for (let x of outerGenerationFn()) {
        console.log(x) // foo baz
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      说明 yield*的值是关联迭代器返回 done:true 时的 value 属性。

    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
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

    # 生成器作为默认迭代器

    因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。

    class Foo {
      constructor() {
        this.values = [1, 2, 3]
      }
      *[Symbol.iterator]() {
        yield* this.values
      }
    }
    const f = new Foo()
    for (const x of f) {
      console.log(x)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # 提前终止生成器

    生成器也支持“可关闭”,生成器对象除了有 next()和 return()方法,还有 throw(),都可以提前终止迭代器。

    上次更新: 2021/09/20, 23:04:19
    第6章 集合引用类型
    第8章 对象、类与面向对象编程

    ← 第6章 集合引用类型 第8章 对象、类与面向对象编程→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×