JavaScript迭代器(iterator)-深入

303 阅读3分钟

迭代器细节

如果对象的原型链上某个对象也实现了迭代协议,那么这个对象也是可迭代对象

迭代器的执行机制

  1. 调用可迭代对象实现的Symbol.iterator函数,这个工厂函数执行后会创建并返回一个新的迭代器,用于迭代其关联的可迭代对象
  2. 每个迭代器内部会维护一个指针,用于标记当前迭代器所处的位置
  3. 第一次调用迭代器的next()方法,指针会指向其关联的可迭代对象的第一个值,并返回一个迭代结果对象,包含done和value两个属性,done迭代是否结束的标志,value为当前值
  4. 第二次调用迭代器的next()方法,指针会移到可迭代对象的第二个值,同时返回迭代结果对象
  5. 继续调用迭代器的next()方法,直到指针指向迭代器的最后一个值,那么下一次调用next()方法时,指针指向的值为空,就可以个返回迭代结束的结果对象,done为true,value可以省略

迭代器执行流程.png

实现了迭代协议的内置对象

  1. 字符串
  2. 数组
  3. 映射
  4. 集合
  5. arguments 对象
  6. NodeList 等 DOM 集合类型

迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

接收可迭代对象的原生语言特性

  1. for-of 循环
  2. 数组解构
  3. 扩展运算符
  4. Array.from()
  5. 创建集合
  6. 创建映射
  7. Promise.all() 接收由Promise实例组成的可迭代对象
  8. Promise.race() 接收由Promise实例组成的可迭代对象
  9. yield* 操作符,在生成器中使用

提前终止迭代器

可以在迭代器中实现可选的reutrn()方法用于指定在迭代器关闭时执行的逻辑,执行迭代的结构在想让迭代器知道它不想让可迭代对象耗尽时,就可以关闭迭代器,比如:

  1. for-of 循环通过break,continue,return或者throw提前退出
  2. 解构操作并未消耗所有值
const iterableObj = {
  [Symbol.iterator]() {
    let limit = 5
    let count = 0
    return {
      next() {
        if (count < limit) {
          return {
            done: false,
            value: count++,
          }
        } else {
          return {
            done: true,
          }
        }
      },
      return() {
        console.log('提前退出了')
        // 必须要返回一个迭代结果类型的对象
        return {
          done: true
        }
      }
    }
  },
}

// 调用可迭代对象的Symbol.iterator方法可以获得迭代器
let iterator = iterableObj[Symbol.iterator]() 

// for-of 提前退出
for(let i of iterableObj) {
  if(i > 2) {
    break
  }
}

// 不完全解构
let [a, b, c] = iterableObj

内置语言结构在发现还有更多值可以迭代,但是不会消费这些值时,就会自动调用return()方法

如果迭代器没有被关闭,那么还可以继续迭代

const iterableObj = {
  [Symbol.iterator]() {
    let limit = 5
    let count = 0
    return {
      // 让迭代器成为可迭代对象
      [Symbol.iterator]() {
        return this
      },
      next() {
        if (count < limit) {
          return {
            done: false,
            value: count++,
          }
        } else {
          return {
            done: true,
          }
        }
      },
      return() {
        console.log('提前退出了')
        // 迭代器指针回退
        count--
        // 关闭迭代器
        // count = 5
        return {
          done: true
        }
      },
    }
  },
}

/**
 * 调用可迭代对象的Symbol.iterator方法可以获得迭代器,由于迭代器也有Symbol.iterator并且执行后返回的就是可迭代对象的迭代器
 * 所以同样可以被拿来迭代,因为for-of如果去直接迭代iterableObj会创建新的可迭代对象,通过闭包维护新的指针值,所以需要通过原来
 * 的迭代器继续使用原来的指针值才能继续迭代
 */
let iterator = iterableObj[Symbol.iterator]()

// for-of 提前退出
for (let i of iterator) {
  if (i > 2) {
    break
  }
  console.log(i) // 0, 1, 2
}
// 如果迭代器没有被关闭,那么还可以继续迭代
for(let i of iterator) {
  console.log(i) // 3, 4
}

// 直接遍历iterableObj的结果
// for-of 提前退出
for (let i of iterableObj) {
  if (i > 2) {
    break
  }
  console.log(i) // 0, 1, 2
}
// 创建了新的迭代器,所以会重新开始迭代
for(let i of iterableObj) {
  console.log(i) // // 0, 1, 2,3, 4
}

如果要关闭迭代器,那么在return()方法中直接将迭代指针指改为结束条件即可,顺便提一下,数组的迭代器就是不会关闭的,所以数组可以像上面那样从离开的地方继续迭代

迭代器练习

这里会有一些迭代器封装和使用的例子,后面会持续补充

迭代器实现斐波拉契序列的产生

// 0 1 1 2 3 5 8
const Fib = {
  [Symbol.iterator]() {
    let n1 = 0,
      n2 = 1,
      index = 0
    return {
      [Symbol.iterator]() {
        return this
      },
      next() {
        if (index < 2) {
          return {
            done: false,
            value: index++,
          }
        } else {
          let temp = n1 + n2
          n1 = n2
          n2 = temp
          return {
            done: false,
            value: temp,
          }
        }
      },
    }
  },
}

let i = Fib[Symbol.iterator]()
let arr = []
for(let i of Fib) {
  if(i > 100) {
    console.log(arr) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
    break
  }
  arr.push(i)
}

参考文献