JavaScript高级深入浅出:生成器与迭代器

874 阅读7分钟

介绍

本文是 JavaScript 高级深入浅出的第 16 篇,本文将会介绍生成器与迭代器相关知识

正文

1. 迭代器

1.1 什么是迭代器

迭代器(Interator)是确使用户可在容器对象(container,如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。

  • 其行为像数据库中的光标,迭代器最早出现在 1974 年设计的 CLU 编程语言中
  • 在各种编程语言的实现中,迭代器的实现方式各不相同,但是很多语言都有迭代器,如 Java、Python 等

从迭代器的定义来看,迭代器是帮助我们对某个数据结构进行遍历的对象

在 JS 中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol)

  • 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
  • 这个在 JS 的标准中就是一个特定的 next 方法

1.2 next 方法规范

next 方法有如下的要求:

  • 一个无参或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
  • done(boolean)
    • 如果迭代器可以产生序列中的下一个值,则为 false (等价于没有 done 这个属性)
    • 如果迭代器已将序列迭代完毕,则为 true,这种情况下,value 是可选的,如果它依然存在,则为迭代结束之后的默认值
  • value
    • 迭代器返回的任何 JS 值,donetrue 时可以省略

1.3 迭代器例子

const names = ['Alex', 'John', 'Alice']

// 创建一个迭代器对象来访问数组
let index = 0
const namesIterator = {
  next() {
    if (index < names.length) {
      return { done: false, value: names[index++] }
    } else {
      return { done: true, value: undefined }
    }
  },
}

console.log(namesIterator.next()) // { done: false, value: 'Alex' }
console.log(namesIterator.next()) // { done: false, value: 'John' }
console.log(namesIterator.next()) // { done: false, value: 'Alice' }
console.log(namesIterator.next()) // { done: true, value: undefined }

1.4 可迭代对象

上面的代码看起来会很奇怪:

  • 我们在获取数组的时候,需要自己创建一个 index 变量,再创建一个所谓的可迭代对象
  • 事实上我们可以对上面的代码进行封装,让其变成一个可迭代对象

什么又是可迭代对象呢?

  • 和迭代器是不同的概念
  • 当一个对象实现了 iterator protocol 迭代器协议时,它就是一个可迭代对象
  • 这个对象的要求时必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问
const namesIterator = {
  names: ['Alex', 'John', 'Alice'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] }
        }
        return { done: true, value: undefined }
      },
    }
  },
}

1.5 原生迭代器对象

我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象:

  • StringArrayMapSetarguments对象、NodeList集合
const names = ['Alex', 'John', 'Alice']
const namesIterator = names[Symbol.iterator]()

console.log(namesIterator.next()) // { value: 'Alex', done: false }
console.log(namesIterator.next()) // { value: 'John', done: false }
console.log(namesIterator.next()) // { value: 'Alice', done: false }
console.log(namesIterator.next()) // { value: undefined, done: true }

1.6 可迭代对象的应用

  • JS 中的语法:for...of展开语法yield*解构赋值
  • 创建一些对象时:new Map([iterable])new WeakMap([iterable])new Set([iterable])new WeakSet([iterable])
  • 一些方法的调用:Promise.all(iterable)Promise.race(iterable)Array.from(iterable)

1.7 自定义类的可迭代性

ArraySetStringMap等创建出来的对象都是可迭代对象

  • 在面向对象的开发中,也可以自己创建一个类,用于创建可迭代对象

案例:创建一个 classroom 类

  • 教室有自己的属性(名称、最大容量、当前教室的学生)
  • 可以进入新的学生 push
  • 创建的教室对象都是可迭代对象
class Classroom {
  constructor(name, maxCount, currentStudents) {
    this.name = name
    this.maxCount = maxCount
    this.currentStudents = currentStudents
  }
  push(student) {
    if (this.maxCount === this.currentStudents.length) return
    this.currentStudents.push(student)
  }
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.currentStudents.length) {
          return { done: false, value: this.currentStudents[index++] }
        }
        return { done: true, value: undefined }
      },
      // 通过 return 可以监听跳出操作
      return: () => {
        console.log('迭代器 break')
        // 记得要 return
        return { done: true, value: undefined }
      },
    }
  }
}

const c1 = new Classroom('6-1', 40, ['Lily', 'Alex', 'John', 'Jason'])
for (const student of c1) {
  console.log(student)
  if (student === 'Alex') break
}
// Lily
// Alex
// 迭代器 break

2. 生成器

2.1 什么是生成器

生成器是 ES6 新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。

正常的函数,终止的条件通常是返回值或出现异常。

生成器函数也是一个函数,但是和普通的函数有一些区别:

  • 首先,生成器函数需要在 function 后面加一个 *
  • 其次,生成器函数可以通过 yield 关键字来控制函数的执行流程
  • 最后,生成器函数的返回值是一个 Generator 生成器,生成器是一种特殊的迭代器
function* foo() {
  console.log('函数开始执行')

  const value1 = 100
  console.log(value1)
  yield

  const value2 = 200
  console.log(value2)
  yield

  const value3 = 300
  console.log(value3)
  yield

  console.log('函数执行完毕')
}

// 在调用生成器函数时,函数内一行代码也不会执行
// 而是返回一个生成器对象
const generator = foo()
// 通过 next 方法来调用每一段 yield 之前的代码
generator.next()
// 函数开始执行
// 100

generator.next()
// 200

generator.next()
// 300

generator.next()
// 函数执行完毕

2.2 生成器函数的返回值

迭代器在执行 next 方法后,是可以返回 { done: Boolean, value: '' },在生成器函数的执行过程中,也是可以返回数据的

function* foo() {
  yield
  return 'finish'
}

const generator = foo()

console.log(generator.next()) // { value: undefined, done: false }
console.log(generator.next()) //  { value: 'finish', done: true }

由此我们可以知道,在生成器函数中 return 的值,将会作为 done 时的 value

那么我们该如何控制每一步执行的 value 呢?yield 后面可以跟随一个值/表达式,将会作为每一步的 value

function* foo() {
  yield 'first'
  yield 'second'
  return 'finish'
}

const generator = foo()

console.log(generator.next()) // { value: 'first', done: false }
console.log(generator.next()) // { value: 'second', done: false }
console.log(generator.next()) // { value: 'finish', done: true }

2.3 生成器的其他方法使用

1. next 传递参数

我们在调用生成器的 next 方法时,可以传入参数,那么该参数会在哪里被接收呢?

function* foo() {
  console.log('第一段代码')
  // 会在这里接收参数
  const input = yield
  console.log('input', input)
  console.log('第二段代码')
}

const generator = foo()

generator.next()
// 第一段代码
generator.next('输入')
// input 输入
// 第二段代码

我们在调用 next() 时传入的参数,将会作为该段代码的上一个 yield 的返回值。

那第一段代码怎么拿到参数呢?可以给生成器函数传入实参。

function* foo(input) {
  console.log('input', input)
}

const generator = foo('传入参数')
generator.next() // input 传入参数

2. 生成器提前结束 - return 方法

function* foo() {
  console.log('第一段代码')
  const input = yield
  console.log('第二段代码')
  console.log('input', input)
  yield
  console.log('第三段代码')
}

const generator = foo()
generator.next() // 第一段代码
const returnValue = generator.return('传入参数')
console.log('returnValue', returnValue) // returnValue { value: '传入参数', done: true }
generator.next() // 不会执行

由上面的代码可以得出,我们可以使用 return 方法来提前中断生成器的执行

const input = yield
return input  // 相当于直接在这里添加了一行代码 return input

3. 生成器抛出异常 - throw 方法

function* foo() {
  console.log('第一段代码')
  yield
  console.log('第二段代码')
  yield
  console.log('第三段代码')
}

const generator = foo()
generator.next() // 第一段代码
generator.throw() // 有了这一段代码,直接报错
generator.next() // 不会执行

由上面的代码可以得出,我们可以使用 return 方法来提前中断生成器的执行,相当于在第一个 yield 的地方抛出了一个异常,需要捕获才会执行下面的代码

function* foo() {
  console.log('第一段代码')
  // 这里捕获异常
  try {
    yield
  } catch (error) {}
  console.log('第二段代码')
  yield
  console.log('第三段代码')
}

const generator = foo()
generator.next() // 第一段代码
generator.throw() // 第二段代码
generator.next() // 第三段代码
// 就可以正常执行了

throw方法也可以传参,参数会被 catch 的参数接收

2.4 生成器替代迭代器

生成器是一种特殊的迭代器,因此在某种应用场景下可以直接使用生成器来替代迭代器

// 迭代器代码
const namesIterator = {
  names: ['Alex', 'John', 'Alice'],
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.names.length) {
          return { done: false, value: this.names[index++] }
        }
        return { done: true, value: undefined }
      },
    }
  },
}
// 生成器代码
const names = ['Alex', 'John', 'Alice']
function* generator(names) {
  for (const item of names) {
    yield item
  }
}

const gen = generator(names)

console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }

事实上我们还可以通过 yield* 生产一个可迭代对象(这个是 yield 的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中一个值)

// 使用 yield*
const names = ['Alex', 'John', 'Alice']
function* generator(names) {
  yield* names
}

const gen = generator(names)

console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }

2.5 自定义类的生成器方案

我们可以使用生成器来重写 1.7 的案例

class Classroom {
  constructor(name, maxCount, currentStudents) {
    this.name = name
    this.maxCount = maxCount
    this.currentStudents = currentStudents
  }
  push(student) {
    if (this.maxCount === this.currentStudents.length) return
    this.currentStudents.push(student)
  }
  // 如果没有 function 关键字,可以在方法名前面写
  *[Symbol.iterator]() {
    yield* this.currentStudents
  }
}

const c1 = new Classroom('6-1', 40, ['Lily', 'Alex', 'John', 'Jason'])
for (const student of c1) {
  console.log(student)
}
// Lily
// Alex
// John
// Jason

3. Promise 的缺陷

在第 15 篇中,介绍了 Promise 是一种异步代码的规范,Promise 真的那么好吗?诚然,它使用起来是非常友好的,但是在某种情况下,还是可能会出现嵌套地狱的情况:

需求:需要多次请求接口,多个接口返回的数据结合在一起,打印最终数据

// requestData 返回一个 Promise
requestData("alex").then(res => {
    requestData(res + 'bbb').then(res2 => {
        requestData(res + 'ccc').then(res3 => {
            requestData(res3 + 'ddd').then(res4 => {
                // .....
            })
        })
    })
})

如果出现上述的场景下,多个嵌套之间可能还会有其他的复杂的业务逻辑,那么还是会出现回调地狱,所以该如何解决呢?

3.1 解决方案一

requestData("alex").then(res => {
    return requestData(res + 'aaa')
}).then(res => {
    return requestData(res + 'bbb')
}).then(res => {
    return requestData(res + 'ccc')
}).then(res => {
    // .....
})

这种方式,可以解决嵌套的问题,但是可读性还是很差的,如果掺杂着逻辑代码,也会看起来很复杂

3.2 解决方案二

这种解决方案,决定采用 Promise + generator 的方式

function* getData(params) {
  const params1 = yield requestData(params)
  const params2 = yield requestData(params1)
  const params3 = yield requestData(params2)
  const params4 = yield requestData(params3)
  console.log(params4)
}

const generator = getData('alex')
// 写一个函数,自动执行生成器
function execGeneratorFn(genFn) {
  const generator = genFn()
  function exec(params) {
    const result = generator.next(params)
    if (result.done) {
      return result.value
    }
    result.value.then(res => {
      exec(res)
    })
  }
  exec()
}

execGeneratorFn(getData)

3.3 解决方案三

我们上述实现的自动执行生成器的代码,在社区中已经有库了 co

npm install co
const co = require('co')
co(getData)  // 这种方式同样也可以

3.4 使用 async/await

自 ES8 开始,JS 支持自动执行生成器

async function getData() {
  const params1 = await requestData('alex')
  const params2 = await requestData(params1 + 'aaa')
  const params3 = await requestData(params2 + 'bbb')
  const params4 = await requestData(params3 + 'ccc')
  console.log(params4)
}

getData()  // 没问题

所以我们就可以知道,其实 async/await 本质上还是 Promise + Generator 的语法糖

总结

本文讲了什么是生成器与迭代器,并通过多个例子来演示。同时,也通过生成器 + Promise 最终引出来 async/await

在下一篇中,将会重点介绍 async、await 以及事件循环