Iterator 与 Generator

IteratorGenerator


# 迭代器

# 可迭代对象

  • 可以简单理解为拥有 [Symbol.iterator] (下称 @@iterator )属性(自身或原型链上都可)的任何一个对象都是可迭代对象
  • 这个属性的值是一个方法,调用这个方法会返回一个迭代器对象
let myIterable = {
  // 生成器函数默认实现了迭代协议
  [Symbol.iterator]: function *() {
     yield 1
     yield 2
     yield 3
  }
}
[...myIterable]; // [1, 2, 3]
1
2
3
4
5
6
7
8
9

上面的例子用了一个生成器函数来定义了 myIterable@@iterator属性,因为生成器函数的返回值本身就是一个迭代器对象(具有 next 方法)

# 迭代器对象

  • 可以简单理解为拥有 next 方法的任何一个对象

  • 调用 next 方法,会返回一个包含 valuedone 属性的对象

  • 如果返回了一个非对象值,则会抛出一个TypeError: iterator.next() returned a non-object value的异常。

let obj = {
  [Symbol.iterator]: function () {
    return {
      called: false,
      next() {
        if (!this.called) {
          this.called = true
          return 0
        }
        return 1
      },
    }
  },
}

for (let val of obj) {
  console.log(val) // Uncaught TypeError: Iterator result 0 is not an object
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 自定义迭代行为

我们可以通过提供自己的 @@iterator 方法,重新定义迭代行为:

// 必须构造 String 对象以避免字符串字面量自动装箱
var someString = new String('hi')

someString[Symbol.iterator] = function () {
  return {
    // 只返回一次元素,字符串 "bye",的迭代器对象
    next: function () {
      if (this._first) {
        this._first = false
        return { value: 'bye', done: false }
      } else {
        return { done: true }
      }
    },
    _first: true,
  }
}
;[...someString] // ['bye']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# JS 内置的可迭代对象

目前所有的内置可迭代对象如下:

  • String
  • Array
  • TypedArray
  • MapSet

它们的原型对象都实现了 @@iterator 方法,还有一个比较特殊的对象,就是函数内部的 arguments 对象,不过它是在自身实现了 @@iterator

let g = void 0
function gen() {
  console.log(Relect.ownKeys(arguments))
  g = arguments[Symbol.iterator]()
}
gen(1, 2, 3, 4)
g.next()
// ['length', 'callee', Symbol(Symbol.iterator)]
// { value: 1, done: false}

g.next() // { value: 2, done: false}
g.next() // { value: 3, done: false}
g.next() // { value: 4, done: false}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 接收可迭代对象的内置 API

  • new Map([iterable])
  • new WeakMap([iterable])
  • new Set([iterable])
  • new WeakSet([iterable])
  • Promise.all(iterable)
  • Promise.race(iterable)
  • Array.from(iterable)

# 需要可迭代对象的语法

一些语句和表达式需要可迭代对象,比如

  • for...of
  • ...
  • yield*
  • 解构赋值

# 提前终止迭代器

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

什么是提前关闭,比如下面几种场景

  • for-of 循环中通过 break、continue、returnthrow 语句提前退出遍历过程

  • 当解构操作并未消费所有值时

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

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方法
      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) // 1, 2, 'Exiting early'
}

let counter2 = new Counter(5)
let [a, b, c] = counter2 // 'Exiting early'
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

因为 return()方法是可选的,所以并非所有迭代器都是可关闭的。

  • 要知道某个迭代器是否可关闭, 可以测试这个迭代器实例的 return 属性是不是函数对象。
  • 不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。因为调用 return() 并不会强制迭代器进入关闭状态
// 判断迭代器是否可以关闭
let itReturn = Array()[Symbol.iterator]().return
typeof itReturn === 'function' // false
1
2
3

# 生成器

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。生成器其实是迭代器的一种具体实现

# 特征

  • 首次调用生成器函数,生成器函数并不会执行任何代码,而是返回一个迭代器对象
  • 通过调用返回对象的 next 方法,Generator 函数才会执行,直到遇到 yield 关键字
  • 箭头函数不能用来定义生成器函数

# 示例

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i
  }
}
// 初次调用,不执行,仅返回一个生成器对象
var a = makeRangeIterator(1, 10, 2)

// yield 后面的值作为每次迭代输出的value
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# yield

yield 关键字有点像函数的中间返回语句,它生成的值(即 yield 后面的表达式的值)会作为 next 方法返回对象的 value 属性的值

遇到 yield 关键字之后,函数就会暂停执行,并把 yield 后面的表达式的值作为返回对象的 value 值返回

  • 通过 yield 关键字退出的生成器函数会处在 { done: false } 状态
  • 通过 return 关键字退出的生成器函 数会处于 { done: true } 状态。
  • yield 关键字只能在生成器函数内部使用,出现在嵌套的非生成器函数中会抛出语法错误
function* gen () {
  yield 1
  yield 2
  yield 3
}
let g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: false} 此处done为false
g.next() // {value: undefined, done: true}

// 仅能在生成器函数内部使用,内部嵌套的非生成器函数中也不能使用
function* gen2() {
  yield 1
  function fn() {
    yield 2 // error
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 使用场景

# 当成自定义的可迭代对象使用

function* gen() {
  yield 1
  yield 2
  yield 3
}

for (let val of gen()) {
  console.log(val) // 1,2,3
}
1
2
3
4
5
6
7
8
9

# 使用 yield 实现输入输出

上一次让生成器函数暂停的 yield 关键字会接收到传给 next 方法的第一个值。第一次调用 next() 传入的值不会被使用,因为这一次调用是为了开始执行生成器函数

相当于传递给 next 方法的值,赋值给了上次让 生成器函数暂停 的 yield 表达式

function* gen() {
  console.log(yield, '1') // 第二次调用 next 方法传进来的 2 会赋值给这里的 yield 关键字
  console.log(yield, '2') // 第三次调用 next 方法传进来的 3 会赋值给这里的 yield 关键字
  return yield '3' // 第四次调用 next 方法传进来的值 4 会赋值给这里的 yield 关键字,并作为 { done: true } 时的 value 返回
}

let g = gen()
g.next(1) // 第一次传入的值会被忽略  {done: false, value: undefined}
g.next(2) // 2, '1' {done: false, value: 2}
g.next(3) // 3, '2' {done: false, value: '3'}
g.next(4) //        {done: true, value: 4}
g.next(5) //        {done: true, value: undefined}
1
2
3
4
5
6
7
8
9
10
11
12

# yield*

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

理解:yield*可以像迭代器一样遍历一个可迭代的对象

// 等价的 generatorFn:
// function* generatorFn() {
//   for (const x of [1, 2, 3]) {
//     yield x;
//   }
// }

function* generatorFn() {
  yield* [1, 2, 3]
}

let generatorObject = generatorFn()
for (const x of generatorFn()) {
  console.log(x)
} // 1 // 2 // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • yield*的值是关联迭代器返回 done: true 时的 value 属性。
  • 对于普通迭代器来说,这个值是 undefined
  • 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值
// ===== 普通迭代器 ====== //
function* gen() {
  console.log('gen value:', yield* [1, 2, 3])
  return 4
}
for (let val of gen()) {
  console.log('val', val)
}
// val 1
// val 2
// val 3
// gen value: undefined

// ====== 生成器函数产生的迭代器 ======= //
function* outerGen() {
  yield* [1, 2, 3]
  return 4
}

function* innerGen() {
  console.log('innerGen:', yield* outerGen())
}
for (let val of innerGen()) {
  console.log('val', val)
}
// val 1
// val 2
// val 3
// innerGen: 4
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

# 使用场景

  • 递归:执行生成器函数返回一个迭代器,而 yield*又可以遍历迭代器
  1. 实现斐波那契数列
function* fibonacciGenerator(a = 0, b = 1) {
  yield a
  yield* fibonacciGenerator(b, a + b)
}
1
2
3
4
  1. 拍平数组
function* flatten(arr) {
  if (Array.isArray(arr) || arr.lenth <= 1) return arr
  for (let item of arr) {
    Array.isArray(arr) ? yield* flatten(item) : yield item
  }
}
1
2
3
4
5
6

# 提前终止生成器

# return()

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

function* generatorFn() {
  for (const x of [1, 2, 3]) {
    yield x
  }
}

const g = generatorFn()

console.log(g) // generatorFn {<suspended>}
console.log(g.return(4)) // { done: true, value: 4 }
console.log(g) // generatorFn {<closed>}
1
2
3
4
5
6
7
8
9
10
11

# throw()

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

# 生成器函数的原理

// 生成器函数
function* test() {
  yield 2
  yield 3
}

// test函数被babel编译之后,变成如下的形式
function test() {
  // 被编译后的 test 函数
  let _test = function (_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2
          return 2

        case 2:
          _context.next = 4
          return 3

        case 4:
        case 'end':
          return _context.stop()
      }
    }
  }
  return regeneratorRuntime.wrap(_test)
}
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

要实现的其实就是 regeneratorRuntime.wrap 这个方法

function regeneratorRuntimeWrap(fn) {
  let obj = {
    next: 0
    stop() {}
  }
  return {
    next() {
      let res = fn(obj)
      if(res !== undefined) return { value: res, done: false }
      return { value: res, done: true }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# thunk

thunk 函数的含义与用法 (opens new window)

使用 thunk 函数实现 Generator 的自动执行

function thunk(gen) {
  // 首次调用 next 方法的返回值是一个 迭代器对象
  let g = gen()

  function next(data) {
    // g.next 的返回值是一个 { value: [[promise]], done: true/false } 的对象
    let { value, done } = g.next(data)
    if (done === true) return data
    // res 是 readFile 的返回值
    value.then((res) => {
      console.log(res)
      next(res)
    })
  }

  next()
}
let a = 0
function readFile(file) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(file + '_' + ++a), 1000)
  })
}

function* gen() {
  let a = yield readFile('1.txt')
  let b = yield readFile(a)
  let c = yield readFile(b)
  return c
}

thunk(gen)
// '1.txt_1'
// '1.txt_1_2'
// '1.txt_1_2_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
28
29
30
31
32
33
34
35
Last Updated: 10/21/2024, 4:15:17 PM