从 Iterator 迭代器到 Generator 生成器

Huy大约 6 分钟javascriptjavascript

迭代器的协议规范是:一个对象必须实现一个特定的接口,该接口包含一个名为 next 的方法,该方法返回一个对象,该对象包含两个属性:valuedone

value 属性表示迭代器返回的当前值,done 属性是一个布尔值,表示迭代器是否已经迭代完所有元素。

{
  value: any,
  done: boolean
}

此外还需要实现 Symbol.iterator 方法,使得 for...of/解构/扩展运算符 等可以遍历该对象。

若数据结构(对象)具备了 Symbol.iterator 属性,则它就是可迭代的,可以被迭代器遍历。并且可以用 for...of 遍历。以类数组举例,默认是不具备 Symbol.iterator 属性的,所以不能被迭代器遍历,也不能用 for...of 遍历。(数组这个属性在原先上)

常见可迭代对象

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • arguments 对象
  • NodeList 对象

虽然类数组对象不具备 Symbol.iterator 属性,但是它们可以通过 Array.from 方法转换为数组,然后使用 Symbol.iterator 属性进行迭代。此外部分类数组也是具备这个原型方法的, 也具有 length 属性。但是它们不是纯数组, 不能直接使用 mapfilter 等数组方法。如 argumentsNodeListHTMLCollection 等。

// node 节点,所有类型节点(Element、Text、Comment...),例如通过 document.querySelector('*') 获取的节点。这个是静态快照,后续节点更改,这个NodeList 不会改变。
NodeList.prototype[Symbol.iterator]

// 元素集合, 仅包含元素节点(Element),例如通过 document.getElementsByClassName('*') 获取的元素集合。动态实时更新,DOM 变化时自动同步
HTMLCollection.prototype[Symbol.iterator]

js 简单实现

实现要点:

  1. next 方法返回一个对象,该对象包含两个属性:valuedone
  2. 当返回的 donetrue 时,表示迭代器已经迭代完所有元素。
  3. 需实现 Symbol.iterator 方法,使得 for...of/解构/扩展运算符 等可以遍历该对象。
  4. 可维护状态,通过 index 记录当前遍历的位置。
class Iterator {
  constructor(items = []) {
    this.items = items // 存储要遍历的元素
    this.index = 0 // 当前索引
  }

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

  // 为了支持 for...of 循环,我们需要在 Iterator 类中实现 Symbol.iterator 方法,该方法返回一个迭代器对象。迭代器对象需要实现 next 方法,该方法返回一个对象,包含 value 和 done 属性。value 是当前迭代的值,done 是一个布尔值,表示迭代是否结束。
  [Symbol.iterator]() {
    return this
  }
}

const iterator = new Iterator([10, 20, 30])

console.log(iterator.next()) // { value: 10, done: false }
console.log(iterator.next()) // { value: 20, done: false }
console.log(iterator.next()) // { value: 30, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

// 支持 for...of
for (const val of new Iterator(['a', 'b', 'c'])) {
  console.log(val) // 输出 'a' 'b' 'c'
}

对象实现迭代

Object.prototype[Symbol.iterator] = function () {
  // const keys = Object.keys(this)
  const keys = Reflect.ownKeys(this) // 相较于 Object.keys,Reflect.ownKeys 可以获取到不可枚举属性和 Symbol 属性。
  let index = 0 // 当前索引

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

const obj = { a: 1, b: 2, c: 3, [Symbol('sym')]: 4 }

for (const val of obj) {
  console.log(val) // 1, 2, 3, 4
}

关于 Reflect.ownKeys 的额外总结

方法能否获取 Symbol key
Object.keys()❌ 只返回字符串 key
Object.getOwnPropertyNames()❌ 只返回字符串 key
for...in❌ 只枚举字符串、可枚举 key
JSON.stringify()❌ 跳过 Symbol key
Reflect.ownKeys()✅ 返回 所有 key (字符串 + Symbol)
Object.getOwnPropertySymbols()✅ 只返回 Symbol key

举例:

const obj = { a: 1, b: 2, c: 3, [Symbol('sym')]: 4 }

Object.keys(obj) // 只返回字符串 key: ['a', 'b', 'c']

for (let key in obj) console.log(key) // 只打印 'a', 'b', 'c'

JSON.stringify(obj) // {"a":1,"b":2,"c":3}

Reflect.ownKeys(obj) // ['a', 'b', 'c', Symbol(sym)]

Object.getOwnPropertySymbols(obj) // [Symbol(sym)]

迭代运行的时间

关于运行效率,这里对比一下 for、while、for...of、for...in、forEach、map 的运行时间。

const arr = new Array(1000000).fill(1)

console.time('for')
for (let i = 0; i < arr.length; i++) {}
console.timeEnd('for') // for: 2.17ms

console.time('while')
let i = 0
while (i < arr.length) {
  i++
}
console.timeEnd('while') // while: 1.141ms

console.time('for...of')
for (const val of arr) {
}
console.timeEnd('for...of') // for...of: 8.81ms

console.time('for...in')
for (const key in arr) {
}
console.timeEnd('for...in') // for...in: 87.847ms

console.time('forEach')
arr.forEach(() => {})
console.timeEnd('forEach') // forEach: 6.422ms

console.time('map')
arr.map(() => {})
console.timeEnd('map') // map: 7.874ms

可以看出 for...in 是最慢的,原因在于:for...in 会遍历所有可枚举属性,包括原型链上的可枚举属性,而其他方法只会遍历当前对象的属性。在实际开发中,因避免使用。

Generator 生成器

Generator 生成器函数式一个特殊的函数,它返回一个迭代器对象。生成器函数使用 function* 语法来定义,函数体内部使用 yield 关键字来定义迭代器的值。

箭头函数无法变为生成器函数

每一个生成器函数,都是 GeneratorFunction 这个类的实例。

fn.__proto__ === GeneratorFunction.prototype

  1. Generator 返回的是迭代器对象,它本身不执行函数体。

    function* foo() {
      console.log('start')
      yield 1
      yield 2
      console.log('end')
    }
    
    const iterator = foo() // 什么都不会打印, 此时状态为 foo {<suspended>}
    
    console.log(iterator.next()) // 执行到第一个 yield: start { value: 1, done: false }
    console.log(iterator.next()) // 执行到第二个 yield: { value: 2, done: false }
    console.log(iterator.next()) // 执行到函数末尾: end { value: undefined, done: true }
    
    • 每次 .next(),函数会「恢复」到上一次 yield 停止处,直到下一个 yield 或结束。
    • 当遇到函数体中的 return,则 donetrue,并且 valuereturn 的值。
    • 如果没有遇到 yieldreturn,运行到函数结尾了,则 donetruevalueundefined
  2. yield 只能在 Generator 函数内部使用,否则会报错。

  3. .next() 可传参数,作为上一个 yield 表达式的返回值。注意:第一次 .next() 传参是无效的,只有从第二次开始生效。

    function* gen() {
      const a = yield 'first'
      console.log('a:', a) // 接手到外部传入值
      yield 'second'
    }
    
    const it = gen()
    
    console.log(it.next()) // { value: 'first', done: false }
    console.log(it.next('hello')) // 打印 'x: hello'
    
  4. Generator 可以手动提前终止,利用 return 和 throw。

    // return
    function* gen() {
      yield 1
      console.log('1') // return 会终止迭代器, 不会执行到这里
      yield 2
      console.log('2')
    }
    
    const it = gen()
    
    console.log(it.next()) // { value: 1, done: false }
    console.log(it.return('hello')) // { value: 'hello', done: true }
    console.log(it.next()) // { value: undefined, done: true}
    
    // throw
    function* gen() {
      try {
        yield 1
      } catch (e) {
        console.log('Caught:', e)
      }
    }
    
    const it = gen()
    console.log('it:', it.next()) // { value: 1, done: false }
    const oops = it.throw(new Error('Oops')) // Caught: Error: Oops
    console.log('oops:', oops) // { value: undefined, done: true }
    console.log('it:', it.next()) // { value: undefined, done: true }
    
  5. Generator 不会自动递归嵌套调用,如果 yield 另一个 Generator,它不会自动遍历里面的值!需要使用 yield* 语法进行委托。

    function* inner() {
      yield 'inner1'
      yield 'inner2'
    }
    
    function* outer() {
      yield 'outer'
      yield inner() // 只是 yield 一个 Generator 对象
    }
    
    for (const v of outer()) {
      console.log(v) // 输出: 'outer' 和 [object Generator]
    }
    

    用 yield* 语法进行委托:

    function* outer() {
      yield 'outer'
      yield* inner() // 展开 inner 的 yield
    }
    
  6. 每次生成的迭代器对象都是独立的,互不影响。

    function* foo() {
      yield 1
    }
    
    const g1 = foo()
    const g2 = foo()
    
    g1.next() // { value: 1, done: false }
    g2.next() // { value: 1, done: false }  // 独立互不影响
    

Generator 模拟 async/await

Generator 自带暂停/恢复特性,非常适合实现 异步控制流,比如配合 Promise 和 co 库来实现同步写异步逻辑。

function* asyncGen() {
  const res = yield fetchData()
  console.log(res) // 这里的 res 是一个 Promise 对象
}

但注意 Generator 本身不是异步函数,它只是暂停函数执行,真正异步控制需要外部配合 Promise 实现。

Loading...