Skip to content

8-1.为什么需要迭代器

我们之前需要遍历数据的时候,通常使用 for 循环。

for 循环其实是利用索引及其递增访问对应的数据。

如果我们只是想要对应的 value,而不想关心 key 的时候,就可以利用迭代器。

目前,数组、Set 集合、Map 集合、字符串都可以使用迭代器

8-2.何为迭代器

迭代器其实是一个对象。

  1. 本身有 next 方法。
  2. 调用 next 方法后,返回值是一个对象。对象有两个属性,valuedone
  3. 其中 value 对应迭代数据的每一项。done 表示是否迭代完毕。迭代完毕时,valueundefined,此时 donetrue

我们可以模拟实现一个迭代器:

js
function getIterator (list) {
  var index = 0
  return {
    next () {
      var done = index >= list.length
      var value = done ? undefined : list[index++]
      return {
        value,
        done
      }
    }
  }
}
var list = ['Tom', 'Jerry']
var iterator = getIterator(list)
iterator.next() // {value: 'Tom', done: false}
iterator.next() // {value: 'Jerry', done: false}
iterator.next() // {value: undefined, done: true}

8-3.默认迭代器

Es6 中,所有的集合对象(数组、 Set 集合、 Map 集合)和字符串都是可迭代对象。

这是因为 Es6 为这些集合对象和字符串都提供了一个方法 Symbol.iterator

Symbol.iterator 本身是 Symbol,它对应的值是一个函数,返回值是迭代器。

js
// 1.数组
var arr = ['i', 'am', 'an', 'array']
var getArrayIterator = arr[Symbol.iterator]
console.log(getArrayIterator) // ƒ values() { [native code] }
// 这里不能使用 getArrayIterator(arr)
var arrayIterator = arr[Symbol.iterator]()
arrayIterator.next()
arrayIterator.next()
arrayIterator.next()
arrayIterator.next()

// 2.Set 集合
var set = new Set(['i', 'am', 'a', 'set'])
var getSetIterator = set[Symbol.iterator]
console.log(getSetIterator) // ƒ values() { [native code] }
var setIterator = set[Symbol.iterator]()
setIterator.next()
setIterator.next()
setIterator.next()
setIterator.next()

// 3.Map 集合
var map = new Map([['id', 19], ['name', 'yxp']])
var getMapIterator = map[Symbol.iterator]
console.log(getMapIterator) // ƒ entries() { [native code] }
var mapIterator = map[Symbol.iterator]()
mapIterator.next()
mapIterator.next()

// 4.字符串
var str = 'i am the king of the world'
var getStrIterator = str[Symbol.iterator]
console.log(getStrIterator) // ƒ [Symbol.iterator]() { [native code] }
var strIterator = str[Symbol.iterator]()
strIterator.next()
strIterator.next()

Es6 中提供了 for...of 这个利用迭代器的新的遍历方式。该方式能遍历所有具有 Symbol.iterator 属性的数据。

利用 Symbol.iterator,我们可以将不可迭代数据转化为可迭代数据。比如对象:

js
var obj = {
  id: 19,
  name: 'yxp'
}
// 对象本身是不可迭代数据 Uncaught TypeError: obj is not iterable
for (const value of obj) {
  console.log(value)
}

// 利用 Symbol.iterator
var iterateObj = {
  id: 19,
  name: 'yxp',
  [Symbol.iterator] () {
    const list = []
    for (const key in this) {
      // 由于 Symbol.iterator 是 Symbol。所以Object.prototype.hasOwnProperty()并不会获取该属性
      if (this.hasOwnProperty(key)) {
        list.push(key)
      }
    }
    let index = 0
    return {
      next () {
        const done = index >= list.length
        const value = done ? undefined : list[index++]
        return {
          value,
          done
        }
      }
    }
  }
}
for (const value of iterateObj) {
  console.log(value)
}

8-4.内置迭代器

Es6 中,这3种类型的集合对象:数组、 Map 集合与 Set 集合,都内建了以下三种迭代器:

  1. entries() 返回一个迭代器,其值为多个键值对。
  2. values() 返回一个迭代器,其值为集合的值。
  3. keys() 返回一个迭代器,其值为集合中的所有键名。
js
// 以数组为例
var list = ['i', 'am', 'a', 'list']
console.log(list.entries().next()) // {value: Array(2), done: false}
console.log(list.values().next()) // {value: 'i', done: false}
console.log(list.keys().next()) // {value: 0, done: false}

另外值得一提的是,字符串、 伪数组(NodeListarguments)也都能使用 for...of 方法,因为它们支持 Symbol.iterator 属性。但并不支持上述迭代器:

js
function fn () {
  for (const value of arguments) {
    console.log(value) // 逐次打印 1 2 3
  }
  console.log(arguments[Symbol.iterator]) // ƒ values() { [native code] }
  console.log(arguments.entries) // undefined
  console.log(arguments.values) // undefined
  console.log(arguments.keys) // undefined
}
fn(1, 2, 3)

8-5.展开运算符

展开运算符可用于任意可迭代对象

展开运算符可以操作可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值,然后按照返回顺序将它们依次返回。

js
// 1.数组
var list = [1, 2, 3]
console.log([...list]) // [1, 2, 3]
// 2.Set
var set = new Set(['a', 'set'])
console.log([...set]) // ['a', 'set']
// 3.Map
var map = new Map([['id', 19], ['name', 'yxp']])
console.log([...map]) // [['id', 19], ['name', 'yxp']]
// 4.字符串
var str = 'i am string'
console.log([...str]) // ['i', ' ', 'a', 'm', ' ', 's', 't', 'r', 'i', 'n', 'g']
// 5.伪数组(NodeList arguments)
function fn () {
  console.log([...arguments]) // [1, 2, 3]
}
fn(1, 2, 3)

8-6.生成器

我们在前几章中有模拟实现迭代器,在 ES6 中提供了专门的生成器来生成迭代器。

生成器是一种返回迭代器的函数,通过 function 关键字后的 * 符号来表示。函数中会用到新的关键字 yield(意为生成)。

js
function* generator () {
  yield 'Hello world!'
  console.log('console', yield 'alibaba')
  yield 'I love ' + 'my life'
}
var iterator = generator()
console.log(iterator) // generator {<suspended>}
console.log(iterator.next()) // {value: 'Hello world!', done: false}
console.log(iterator.next()) // {value: 'alibaba', done: false}
// console undefined
console.log(iterator.next()) // {value: 'I love my life', done: false}
console.log(iterator.next()) // {value: undefined, done: true}

可以发现:

  • generator 生成器函数执行之后,会返回迭代器 iterator

  • yield 后的值会对应 next 方法执行后的 value 属性。

  • yield 关键字后面可以接任何值或表达式。但在函数内的执行结果是 undefined,并不会有返回值。

  • 执行完 yield 后,该 yield 后的语句不会继续执行,直到再次调用迭代器的 next() 方法才会继续执行。该特性在下一章的高级迭代器功能中有更加广泛的应用。

TIP

yield 只能在生成器的函数作用域内使用,不能在其他函数作用域下使用。

我们使用 for 循环和 forEach 来作比较:

js
// 1.for循环
function* firstGenerator(list) {
  for (let i = 0; i < list.length; i++) {
    yield list[i]
  }
}
var firstIterator = firstGenerator(['Hello', 'World'])
console.log(firstIterator.next()) // {value: 'Hello', done: false}
console.log(firstIterator.next()) // {value: 'World', done: false}
console.log(firstIterator.next()) // {value: undefined, done: true}

// 2.forEach
function* secondGenerator(list) {
  // 由于 yield 在箭头的作用域内,所以执行时会报错
  list.forEach(item => {
    yield item
  })
}
var secondIterator = secondGenerator(['Hello', 'World'])
console.log(secondIterator.next()) // Uncaught SyntaxError: Unexpected identifier
console.log(secondIterator.next())
console.log(secondIterator.next())

此外,由于生成器函数本身也是函数,所以也可以使用函数表达式或者作为对象方法使用:

js
// 1.函数表达式
var generator = function* () {
  yield 'Hello world'
}
var iterator = generator()
console.log(iterator.next()) // {value: 'Hello world', done: false}
js
// 2.作为对象方法使用
var obj = {
  *generator () {
    yield 'Hello world'
  }
}
var iterator = obj.generator()
console.log(iterator.next()) // {value: 'Hello world', done: false}

8-7.高级迭代器功能

在之前,我们已经阐述了迭代器生成器的基本使用。

两者结合,我们来看下高级迭代器功能。

8-7-1.迭代器传参

js
// 1.正常执行
function* generator () {
  var value = yield 1
  var result = yield value + 2
  yield result
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: NaN, done: false} 因为value是undefined,undefined进行加法运算时,结果是NaN
console.log(iterator.next()) // {value: undefined, done: false}

// 2,传参
function* generator () {
  var value = yield 1
  var result = yield value + 2
  yield result
}
var iterator = generator()
console.log(iterator.next(100)) // {value: 1, done: false}
console.log(iterator.next(100)) // {value: 102, done: false}
console.log(iterator.next(100)) // {value: 100, done: false}

// 3.next传参其实影响的是上一次的yield语句的返回值
function* generator () {
  var value = yield 1
  var result = yield value + 2
  yield 2
  yield result
}
var iterator = generator()
console.log(iterator.next(100)) // {value: 1, done: false}
console.log(iterator.next(100)) // {value: 102, done: false}
console.log(iterator.next(200)) // {value: 2, done: false} 可以从这里看出来,next的传参影响到了var result这里的语句。
console.log(iterator.next(100)) // {value: 200, done: false}

总结:

  1. yield 后接变量与表达式时,可进行 next 函数传参操作
  2. 每一次的 next 函数被调用,只会执行对应 yield 右侧语句,左侧语句并不会执行
  3. next 传参其实影响的是上一次的 yield 语句的返回值。

8-7-2.迭代器抛错

js
// 利用try...catch捕获throw抛出的错误
function* generator () {
  var value
  try {
    value = yield 1
  } catch (err) {
    value = 2
    console.warn(err)
  }
  var result = yield value + 2
  yield result
}
var iterator = generator()
// ① 
console.log(iterator.next(100)) // {value: 1, done: false}
// ② 
console.log(iterator.throw(new Error('error'))) // {value: 4, done: false}
// ③
console.log(iterator.next(100)) // {value: 100, done: false}

逐步分析(console 语句):

  • 执行 时,代码执行 var value; yield 1
  • 执行 时,在执行 yield value + 2,会先执行 try...catch 块。由于 throw 抛出错误,这时 try...catch 块捕获,代码执行 catch 块代码,value 被赋值为 2。同时打印 Error。另外 throw 可以看做额外的一次的 next 调用,执行 yield value + 2
  • 执行 时,由于 next 传入的参数是 100,所以 value 会是 100

现在我们修改下 throw 的调用时机:

js
// throw抛出的错误必须在执行yield之前 才能捕获到
function* generator () {
  var value
  try {
    value = yield 1
  } catch (err) {
    value = 2
    console.warn(err)
  }
  var result = yield value + 2
  yield result
}
var iterator = generator()
// ① 
console.log(iterator.throw(new Error('error'))) // Uncaught Error: error
// ② 
console.log(iterator.next(100)) // not work
// ③
console.log(iterator.next(100)) // not work

执行之后,我们会发现 直接报错,原因就是错误未捕获Uncaught Error: error。后面的 不会再执行。

其实这个例子的执行结果,我们在上例中是可以推断出来的:

在执行 时,执行的会是 var value; yield 1 这两步,由于这两步没有 try...catch 进行捕获,在 throw 语句触发的时候,就直接报错阻断代码执行了。

总结

  1. throw 可用于抛出错误。
  2. throw 可以看做会额外执行一次 next 方法。
  3. 在与 throw 对应yield 代码执行过程中,我们可以用 try...catch 捕获错误。

8-7-3.生成器的return

由于生成器也是函数,因此也可以通过 return 语句提前退出函数执行。

当使用 return 时,属性 done 会被设置为 true

如果 return 语句后面有值,属性 value 会被设置成对应的值。

如下例:

js
function* generator () {
  yield 1
  return 'hello world'
  yield 2
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 'hello world', done: true}
console.log(iterator.next()) // {value: undefined, done: true}

8-7-4.委托生成器

在某些情况下,我们可能需要将多个迭代器合而为一。这时我们就可以使用委托生成器

要额外注意的一点是,在定义委托生成器时,需要在 yield 后面添加 *

js
function* numberGenerator () {
  yield 1
  yield 2
}
function* colorGenerator () {
  yield 'red'
  yield 'green'
  yield 'blue'
}
function* combinedGenerator () {
  // 这里的yield必须添加* 否则next方法不能正确迭代
  yield* numberGenerator()
  yield* colorGenerator()
}
var combinedIterator = combinedGenerator()
console.log(combinedIterator.next()) // {value: 1, done: false}
console.log(combinedIterator.next()) // {value: 2, done: false}
console.log(combinedIterator.next()) // {value: 'red', done: false}
console.log(combinedIterator.next()) // {value: 'green', done: false}
console.log(combinedIterator.next()) // {value: 'blue', done: false}

另外还可以结合 return 使用:

js
function* subGenerator () {
  yield 1
  return 3
}
function* generator () {
  var result = yield* subGenerator()
  console.log(result)
  for (let i = 1; i <= result; i++) {
    yield i
  }
}
var iterator = generator()
console.log(iterator.next()) // {value: 1, done: false}
// 3
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 2, done: false}
console.log(iterator.next()) // {value: 3, done: false}

我们可以发现,子生成器中的 return 语句的值,并没有打印 {value: 3, done: true},而是将值传递给了 result 变量。

我们记住这个特点就可以。

8-8.任务执行

我们已经知道,生成器中的 yield 语句是在迭代器的 next 函数调用后才会执行的。

但在实际的业务中,我们并不会根据时机或条件逐步调用 next 函数。

对应 async await 更加高级的处理任务的功能,我们来用迭代器生成器来实现相同的功能。

首先分析下,我们要实现的功能:

js
// ajax
var getUserNameById = function (data, success, error ) {
  $.ajax({
    url: '',
    method: 'get',
    data,
    success,
    error
  })
}
// 生成器 我们的业务功能会写在这里
function* generator () {
  // 1.同步任务
  var value = yield 1
  console.log(value) // 我们根据前几节 会知道这里是undefined 但我们现在在上下文环境 希望它打印的是1
  var result = yield value + 2
  console.log(result)
  // 2.异步任务
  try {
    var res = yield getUserNameById({
      id: 19
    })
    // 对应success的处理逻辑
  } catch (err) {
    // 对应error的处理逻辑
  }
}
// 我们要写一套代码 自动执行上面生成器 并实现下列功能:
/*
1.yield 语句自动依次执行。
2.yield 左侧语句的变量,对应上一步yield执行后的value
3.异步任务按照上面的书写方式执行
4.处理异步任务的成功逻辑与失败逻辑
*/

我们循序渐进的来实现这套函数。

8-8-1.简单任务执行

js
function* generator () {
  console.log(1)
  yield
  console.log(2)
  yield
  console.log(3)
}
// 不封装函数的话 想要打印的话 需要调用3次next函数
var iterator = generator()
iterator.next()
iterator.next()
iterator.next()

现在我们封装一套函数,使得上面的 generator 内的代码能够自动执行,而无需我们再手动的调用 next 函数。

js
// 生成器
function* generator () {
  console.log(1)
  yield
  console.log(2)
  yield
  console.log(3)
}
// 辅助函数
function exec (generatorFn) {
  var iterator = generatorFn()
  var result = iterator.next()
  function step () {
    console.log(result)
    if (!result.done) {
      result = iterator.next()
      step()
    }
  }
  step()
}
exec(generator)

8-8-2.任务执行传参

上节已经实现了同步任务的自动执行,但是如果我们想要实现下面的生成器功能,就需要对辅助函数加以改造、添加传参功能。

js
// 生成器
function* generator () {
  var value = yield 1
  // !!! 如果想要result为3,那么则需要把上一步yield中的得到的value传入next方法
  var result = yield value + 2
  yield result
}
// 1.辅助函数
function exec (generatorFn) {
  var iterator = generatorFn()
  var result = iterator.next()
  function step () {
    console.log(result)
    if (!result.done) {
      result = iterator.next()
      step()
    }
  }
  step()
}
exec(generator)
/*
根据前几节的学习,我们会知道,这里依次打印:
{value: 1, done: false}
{value: NaN, done: false}
{value: undefined, done: false}
{value: undefined, done: true}
*/ 

// 2.可传参的辅助函数
function exexWithParams (generatorFn) {
  var iterator = generatorFn()
  var result = iterator.next()
  function step () {
    console.log(result)
    if (!result.done) {
      // 这里把yield得到的value 传入下一步的next方法中
      result = iterator.next(result.value)
      step()
    }
  }
  step()
}
exexWithParams(generator)
/*
依次打印:
{value: 1, done: false}
{value: 3, done: false}
{value: 3, done: false}
{value: undefined, done: true}
*/

8-8-3.异步任务执行

js
var getImage = function (data) {
  return function (success, error) {
    $.ajax({
      url: 'https://img3.doubanio.com/img/files/file-1633834533-0.jpg',
      method: 'get',
      data,
      success,
      error,
      xhrFields: {
        responseType: 'blob'
      }
    })
  }
}
function* generator () {
  // 1.同步任务
  var value = yield 1
  var result = yield value + 2
  // 2.异步任务
  try {
    var res = yield getImage()
    // 对应success的处理逻辑
    const objectURL = URL.createObjectURL(new Blob([res], { type: 'image/jpeg' }))
    console.log(objectURL)
  } catch (err) {
    // 对应error的处理逻辑
    console.error(err)
  }
}
function exec (generatorFn) {
  var iterator = generatorFn()
  var result = iterator.next()
  function step () {
    console.log(result)
    if (!result.done) {
      // 判断value的类型 异步任务这里会是function
      if (typeof result.value === 'function') {
        var success = function (res) {
          result = iterator.next(res)
          step()
        }
        var error = function (err) {
          // 注意这里的iterator.throw 其实是在 yield getImage() 的下一个yield执行时抛错
          result = iterator.throw(err)
          // 这里是否调用step的区别在于 异步任务抛错后 后续代码是否还要执行
          // step()
        }
        result.value(success, error)
      } else {
        result = iterator.next(result.value)
        step()
      }
      // 因为上面的error本身是异步的 所以step函数本身不能写在这里
      // step()
    }
  }
  step()
}
exec(generator)