JS高级 - 迭代器 和 生成器

190 阅读15分钟

迭代器

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

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

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

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

  • 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
  • 在JavaScript中这个标准就是一个特定的next方法
  • 这个next需要满足如下要求:
    • 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象
      • done(boolean):
        • 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)

        • 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值(一般情况下,对应的默认值为undefined,因此此时value就可以省略)

          • value:
            • 迭代器返回的任何 JavaScript 值。done 为 true 时可省略
const user = {
  friends: ['Alex', 'Klaus', 'Steven']
}

function genIterator(arr) {
  let index = 0

  return  {
    next() {
      if (index < arr.length) {
        return {
          done: false,
          value: arr[index++]
        }
      } else {
        return { done: true }
      }
    }
  }
}

// 此时iterator就是一个迭代器
// 一个专门用于迭代数组的迭代器
const iterator = genIterator(user.friends)

console.log(iterator.next()) // => { done: false, value: 'Alex' }
console.log(iterator.next()) // => { done: false, value: 'Klaus' }
console.log(iterator.next()) // => { done: false, value: 'Steven' }
console.log(iterator.next()) // => { done: true }

可迭代对象

对于上述的示例代码而言,可以发现的是,迭代器和对应需要迭代的对象是分离的,并不内聚,所以我们需要对上述代码进行改进

const user = {
  friends: ['Alex', 'Klaus', 'Steven'],

  // Symbol.iterator作为计算属性名,作为一个特殊的函数
  // 这个函数用于在对象中返回对应的迭代器
  [Symbol.iterator]() {
    let index = 0

    // 迭代器函数中的this为迭代器函数所在的那个对象
    // 因为其内部调用方式类似 user[Symbol.iterator]()
    console.log(this === user) // => true

    return  {
      // 使用箭头函数 修正next函数中的this指向
      // 内部调用方式 类似 [Symbol.iterator]().next()
      next:() => {
        if (index < this.friends.length) {
          return {
            done: false,
            value: this.friends[index++]
          }
        } else {
          return { done: true }
        }
      }
    }
  }
}

// 因为user对象实现了迭代器函数,那么就可以认为user对象是一个可迭代对象
// 任何一个可迭代对象都可以通过for-of进行迭代
// for-of在迭代过程中会自动执行对象内部的[Symbol.iterator]方法
for (const name of user) {
  console.log(name)
}
/*
  =>
    Alex
    Klaus
    Steven
*/

所以如果一个对象需要是可迭代对象,则必须满足如下条件:

  1. 实现迭代器方法,[Symbol.iterator]
  2. 迭代器方法必须返回一个用于迭代当前对象的迭代器

JS原生的一些数据结构(如字符串,数组,set,map)等都是可迭代对象,默认就实现了迭代器方法

const arr = [1, 2, 3]

console.log(arr[Symbol.iterator]()) // => Object [Array Iterator] {}
const iterator = arr[Symbol.iterator]()

console.log(iterator.next()) // => { value: 1, done: false }
console.log(iterator.next()) // => { value: 2, done: false }
console.log(iterator.next()) // => { value: 3, done: false }
console.log(iterator.next()) // => { value: undefined, done: true }

所以当一个对象实现了iterable protocol协议时,它就是一个可迭代对象

可迭代对象和迭代器是不同的概念

如果一个对象实现了 @@iterator 方法,我们就认为这个对象实现了iterable protocol协议,是个可迭代对象

在代码中我们使用 Symbol.iterator 访问该属性

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

例如: String、Array、Map、Set、arguments对象、NodeList集合

示例

const user = {
  name: 'Klaus',
  age: 23,
  height: 1.88,
  [Symbol.iterator]() {
    const entires = Object.entries(user)
    let index = 0

    return {
      next() {
        if (index < entires.length) {
          return { done: false, value: entires[index++] }
        } else {
          return { done: true }
        }
      }
    }
  }
}

for (const [key, value] of user) {
  console.log(key, value)
}
/*
  =>
    name Klaus
    age 23
    height 1.88
*/

应用场景

  1. JavaScript中语法: for ...of、展开语法(spread syntax)、yield*、解构赋值(Destructuring_assignment) {对象的解构赋值除外}
  2. 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])
  3. 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)

for-in vs for-of

for-in是用于遍历对象上的可枚举属性,包括自身的可枚举属性和位于原型链上的可枚举属性

for-of是用于对对象进行迭代,在遍历的时候,会自动去调用对象上的Symbol.iterator方法

也就是说如果一个对象没有实现Symbol.iterator方法,那么该对象是不可迭代对象,不能使用for-of迭代

const user = {
  name: 'Klaus',
  address: 'shanghai',
  __proto__: {
    age: 23
  },
  [Symbol.iterator]() {
    const entries = Object.entries(this)
    let index = 0

    return {
     next() {
      if (index < entries.length) {
        return { done: false, value: entries[index++] }
      } else {
        return { done: true }
      }
     }
    }
  }
}

// for-in会遍历自身和自身原型对象上的所有可枚举属性
for (const key in user) {
  console.log(key)
  /*
    =>
      name
      address
      age
  */
}

// 默认情况下,原生对象是不可迭代对象
// 但是在本例中,自主实现了Symbol.iterator方法
// 所以本例中的user对象是可迭代对象
for (const key of user) {
  console.log(key)
  /*
    =>
      ['name', 'Klaus' ]
      ['address', 'shanghai' ]
  */
}

自定义类的迭代

class Person {
 constructor(name, age, height) {
  this.name = name
  this.age = age
  this.height = height
 }

 // 当一个类的显示原型上存在 可迭代方法([Symbol.iterator])
 // 那么这个类所创建出来的所有实例方法都默认是可迭代对象
 [Symbol.iterator]() {
  const values = Object.values(this)
  let index = 0

  return {
    next: () => {
      if (index < values.length) {
        return { done: false, value: values[index++] }
      } else {
        return { done: true }
      }
    }
  }
 }
}

const p1 = new Person('Klaus', 23, 1.88)

for (const value of p1) {
  console.log(value)
}

/*
  =>
    Klaus
    23
    1.88
*/

迭代器的中断

迭代器在某些情况下会在没有完全迭代的情况下中断:

  • 比如遍历的过程中通过break、return、throw中断了循环操作
  • 比如在解构的时候,没有解构所有的值

那么这个时候我们想要监听中断的话,可以添加return方法

const user = {
  name: 'Klaus',
  age: 23,
  height: 1.88,
  [Symbol.iterator]() {
    const values = Object.values(user)
    let index = 0

    return {
      next() {
        if (index < values.length) {
          return { done: false, value: values[index++] }
        } else {
          return { done: true }
        }
      },

      // 如果没有编写return方法,默认会存在默认的return方法
      // 但是如果编写了return方法,就会对默认的return方法进行重写
      // return方法也是需要返回含有done属性或value属性的一个对象的
      return() {
        console.log('迭代器被中断了')
        return { done: true }
      }
    }
  }
}

for (const value of user) {
  console.log(value)
  if (value === 23) {
    break
  }
}

生成器

生成器是ES6中新增的一种函数控制、使用的方案,

平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常

但是生成器可以让我们更加灵活的控制函数什么时候继续执行、暂停执行

所以 生成器函数本质上是一种特殊的函数

  • 首先,生成器函数需要在function的后面加一个符号*
  • 其次,生成器函数可以通过yield关键字来控制函数的执行流程
  • 最后,生成器函数的返回值是一个Generator(生成器)
    • 而生成器本质上是一种特殊的迭代器
// 一个函数function后边加上 星号 就表明当前函数是生成器函数
// *只要在function和函数名之间即可 即 function* foo() 和 function *foo() 都是合法的
// 推荐 function* foo()

// 生成器方法被执行的时候,并不会执行函数内部的代码
// 而是会返回一个生成器对象
function* foo() {
  console.log(111)
  console.log(222)
  yield console.log('aaa')
  console.log(333)
  console.log(444)
  yield
  console.log(555)
  console.log(666)
}

const generator = foo()

// 当我们执行生成器对象的next方法的时候
// 会自动执行到下一个yield所在位置后停止执行
// yield后边的同行代码 也会被执行
generator.next()
/*
  =>
    111
    222
    aaa
*/

console.log('------')
generator.next()
/*
  =>
    333
    444
*/

console.log('------')
generator.next()
/*
  =>
    555
    666
*/

// 函数内部代码执行完毕后,
// 继续执行生成器对象的next方法
// 那么对应的next方法会静默失效
generator.next()

返回值

function* foo() {
  console.log(111)
  console.log(222)
  yield '第一次yield返回值'
  console.log(333)
  console.log(444)
  yield '第二次yield返回值'
  console.log(555)
  console.log(666)
  return 'foo函数执行完毕了'
}

const generator = foo()

// yield关键字 后边的值 会作为next方法返回值的value值
// 如果是因为 yield关键字 暂停的 函数执行 那么next方法的返回值的done属性的值为false
// 如果是因为 return 结束的 函数执行 那么next方法的返回值的done属性的值为true
console.log(generator.next()) // => { value: '第一次yield返回值', done: false }
console.log(generator.next()) // => { value: '第二次yield返回值', done: false }
console.log(generator.next()) // => { value: 'foo函数执行完毕了', done: true }

传参

  • 调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值
  • 也就是说我们是为本次的函数代码块执行提供了一个值
function* foo(res) {
  console.log(res)
  const res1 = yield '第一次yield返回值'
  console.log(res1)
  const res2 = yield '第二次yield返回值'
  console.log(res2)
  return 'foo函数执行完毕了'
}

// 函数执行到第一个yield中使用的变量在函数调用的时候进行传入
const generator = foo('第一次传参')

// 第一次调next方法时候,一般不会传入对应的参数(也无法传入对应的参数)
generator.next()
// 自第二次开始,next方法传入的参数
// 会作为yield表达式的返回值
generator.next('第二次传参')
generator.next('第三次传参')

中断

function* foo(res) {
  console.log(res)
  const res1 = yield '第一次yield返回值'
  console.log(res1)
  return 'foo函数执行完毕了'
  console.log(res2)
  yield 'foo函数执行完毕了'
}

const generator = foo()

// 如果提前return了,那么生成器函数后边的所有yield和return都不会再被执行
console.log(generator.next('第一次传参')) // => { value: '第一次yield返回值', done: false }
console.log(generator.next('第二次传参')) // => { value: 'foo函数执行完毕了', done: true }
console.log(generator.next('第三次传参')) // => { value: undefined, done: true }

生成器提前结束

function* foo() {
  console.log('part1')
  yield
  console.log('part2')
  yield
  console.log('part3')
}

const generator = foo('arg1')

console.log(generator.next()) // => { value: undefined, done: false }

// 调用生成器的return方法 会立即结束生成器函数的执行
// 返回的对象中 done的值 为 true, value的值为return方法传入的参数
console.log(generator.return('arg2')) // => { value: 'arg2', done: true }
console.log(generator.next('arg3')) // => { value: undefined, done: true }

生成器抛出异常

function* foo() {
  console.log('part1')
  try {
    // 第一个yield的返回值是一个异常,或者说yeild表达式执行产生了异常
    // 所以使用try-catch进行异常捕获
    yield
    console.log('part2')
  } catch(e) {
    // 在catch语句中不能继续yield新的值,但是可以在catch语句外使用yield继续中断函数的执行
    // 在catch中的yield会被自动忽略
    console.log('error:' + e)
  }

  yield
  console.log('part3')
}

const generator = foo('arg1')

console.log(generator.next())
// generator.throw方法 用于让generator在内部抛出一个异常
// 该方法的参数是一个异常对象,其会作为上一个yield(在这里就是第一个yield)的异常被抛出
console.log(generator.throw(new Error('this is an error')))
console.log(generator.next('arg3'))

示例

// 有[a, b] 依次生成 a 到b之间的各个数
// 例如 [1, 4] -> 1, 2, 3, 4
function* printNum(start, end) {
  for (let i = start; i <= end; i++) {
    yield i
  }
}

const generator = printNum(3, 5)

console.log(generator.next()) // => { value: 3, done: false }
console.log(generator.next()) // => { value: 4, done: false }
console.log(generator.next()) // => { value: 5, done: false }
console.log(generator.next()) // => { value: undefined, done: true }

生成器 替代 迭代器

const user = {
  name: 'Klaus',
  age: 24,
  height: 1.88,

  // 生成器函数,会返回一个生成器
  // 生成器本质上是一个特殊的迭代器
  *[Symbol.iterator]() {
    const entries = Object.entries(user)
    for (let i = 0; i < entries.length; i++) {
      // 调用next方法后,会将第一个entry返回,并停止当前函数的执行
      // 再次调用next方法,返回后一个entry,并再次停止当前函数的执行
      // 依次类推
      // 最终迭代完成后,返回{ done: true, value: undefined }
      yield entries[i]
    }
  }
}

for(let [key, value] of user) {
  console.log(key, value)
}
/*
  =>
    name Klaus
    age 24
    height 1.88
*/

yield*

yield* <iterable> 是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值

class Person {
  constructor(name, age, height) {
   this.name = name
   this.age = age
   this.height = height
  }

  *[Symbol.iterator]() {
    yield* Object.values(this)
  }
 }

 const p1 = new Person('Klaus', 23, 1.88)

 for (const value of p1) {
   console.log(value)
 }

 /*
   =>
     Klaus
     23
     1.88
 */

async/await

异步处理方案

如果我们存在如下需求:

  • 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求
  • 第二次的请求url依赖于第一次的结果
  • 第三次的请求url依赖于第二次的结果
  • 依次类推

解决方法一:

function asyncFn(res) {
  return new Promise(resolve => {
    setTimeout(() => resolve(res), 2000)
  })
}

// 这么处理可以解决依次异步请求的问题
// 但是这么做会导致回调函数嵌套回调函数
// 如果请求过多,会导致嵌套层级过多
// 这种情况被称之为回调地狱
asyncFn('第一次调用').then(res => {
  console.log(res)
  asyncFn('第二次调用').then(res => {
    console.log(res)
    asyncFn('第三次调用').then(res => console.log(res))
  })
})

解决方法二:

function asyncFn(res) {
  return new Promise(resolve => {
    setTimeout(() => resolve(res), 2000)
  })
}

// 利用promise的then方法依旧会返回一个新的promise的特性
// 可以将嵌套调用 转换为 链式调用
asyncFn('第一次调用').then(res => {
  console.log(res)
  return asyncFn('第二次调用')
}).then(res => {
  console.log(res)
  return asyncFn('第三次调用')
}).then(res => console.log(res))

解决方法三:

function asyncFn(res) {
  return new Promise(resolve => {
    setTimeout(() => resolve(res), 2000)
  })
}

// 利用生成器函数来处理异步方法
function* fetchData() {
  const res1 = yield asyncFn(10)
  const res2 = yield asyncFn(20)
  const res3 = yield asyncFn(30)

  console.log(res1 + res2 + res3) // => 60
}

const generator = fetchData()

// generator.next().value 就是asyncFn 返回的那个promise对象
generator.next().value
  .then(res => generator.next(res).value)
  .then(res => generator.next(res).value)
  .then(res => generator.next(res).value)
  .catch(err => console.error(err))

上述代码的生成器执行是重复的逻辑,所以我们可以将上述生成器执行过程,封装成一个自动化执行函数

function asyncFn(res) {
  return new Promise(resolve => {
    setTimeout(() => resolve(res), 2000)
  })
}

// 利用生成器函数来处理异步方法
function* fetchData() {
  const res1 = yield asyncFn(10)
  const res2 = yield asyncFn(20)
  const res3 = yield asyncFn(30)

  console.log(res1 + res2 + res3) // => 60
}

const generator = fetchData()
execGenerator(fetchData)

function execGenerator(fn) {
  if (typeof fn !== 'function') {
    throw new Error('fn is not a function')
  }

  const generator = fn()

  function exec(res) {
    const result = generator.next(res)

    // 如果result的done值为true表明的是
    // 函数已经return了,所以直接return即可
    if (result.done) return
    result.value.then(res => exec(res))
  }

  exec()
}

在ES9中,提供了上述写法的语法糖,那就是async/await

所以async/await本质就是promise+generator的语法糖

function asyncFn(res) {
  return new Promise(resolve => {
    setTimeout(() => resolve(res), 2000)
  })
}

// 这段代码就是上述示例的语法糖写法
async function fetchData() {
  const res1 = await asyncFn(10)
  const res2 = await asyncFn(20)
  const res3 = await asyncFn(30)

  console.log(res1 + res2 + res3) // => 60
}

fetchData()

异步函数

异步函数是使用async关键字声明的一个函数,其本质是生成器函数和promise组合使用的一种语法糖写法

异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行

返回值

异步函数有返回值时,和普通函数会有区别:

  • 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中
  • 情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定
  • 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定
async function foo() {
  return 123 // Promise.resolve(123)
}

foo().then(res => console.log(res))
async function foo() {
  return new Promise((resolve) => {
    resolve(123)
  })
}

foo().then(res => console.log(res))

异步函数抛出异常,既可以通过try-catch捕获,也可以通过catch方法捕获

async function foo() {
  throw new Error('this is a error')
}

foo().then(res => console.log(res))
  .catch(res => console.log(res.message))
async function foo() {
  throw new Error('this is a error')
}

try {
  // 等待foo执行结束,以便于捕获foo执行异常
  await foo()
} catch(e) {
  console.error(e.message)
}

await

async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的

  • 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise
  • 那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数
  • await后 也可以跟上普通的同步表达式,对应的结果会使用Promise.resolve进行包裹,但是这么做的结果和不加await关键字时效果是一致的
function fun() {
  return new Promise(resolve => {
    setTimeout(() => resolve('success'), 1000)
  })
}

async function foo() {
  // await后边 如果是一个Promise
  // 会等待到 Promise有结果后,才会继续往后执行代码
  // 如果是resolved 则会把结果赋值给res
  // 如果是rejected 则会抛出对应的异常
  //  + 可以在异步函数中使用 try-catch进行捕获
  //  + 如果异步函数中没有捕获,因为异步函数返回一个promise
  //    所以可以在foo().catch() 中进行捕获
  //  + 如果依旧没有捕获对应的异常,会一层层向上传递
  //    最终交给浏览器进行处理,浏览器会将对应的错误输出在console中
  const res = await fun()
  console.log(res)
}

foo()
async function foo() {
  // await 123 === await Promise.resolve(123)
  // 但是这么做往往没有什么实际意义,因为其最终执行效果和没有await关键字是一致的
  const res = await 123
  console.log(res) // => 123
}

foo()

await关键字只能使用在async函数内部,或ES模块的顶层

使用在async函数内部

async function main() {
  const asyncMsg = Promise.resolve('hello world!')
  console.log(asyncMsg) // => hello world!
}

main()

在ES模块顶层使用

因为顶层await只能使用在ES模块中,所以需要进行特殊设置

方式一: 在package.json中设置type: module

方式二: 将需要开启顶层await的模块的后缀设置mjs

index.mjs

const asyncMsg = await Promise.resolve('hello world!')
console.log(asyncMsg) // "hello world!"