JavaScript 迭代器、可迭代对象与生成器详解

作者:资源驿站时间:2026-10-25 20:23:19浏览:244

本文详细解析 JavaScript 中的迭代器、可迭代对象与生成器。介绍迭代器协议中 next 方法的 done 与 value 属性,讲解如何封装高内聚的迭代器。阐述可迭代对象通过 Symbol.iterator 方法实现 for...of 遍历,列举 String、Array 等内置类型。最后深入生成器函数 function* 的定义、yield 关键字的分段执行与传参机制,以及 yield* 的展开语法。

1. 迭代器的定义

概念:迭代器(iterator)是一种允许用户遍历容器对象(如链表或数组)的对象[1][2][3],其设计目的是让用户无需关心容器对象内存分配的具体实现细节。 JavaScript中的迭代器

  • 其本质是一个符合迭代器协议(iterator protocol)的对象。
  • 迭代器协议

    该对象包含一个next函数。

    调用next函数返回一个对象,该对象包含两个属性。
    • done(完成),它的值为布尔类型,即。true/false。
      • 若迭代器尚未完成遍历,则返回{done:false}。
      • 若迭代器已完成遍历,则返回{done:true}。
    • value(值),它可以返回JavaScript中的任何值,在TypeScript中表示可为:value:any类型。

1.1 迭代器的基本实现

请思考以下代码:

let index = 0
const bears = ['ice', 'panda', 'grizzly']

let iterator = {
  next() {
    if (index < bears.length) {
      return { done: false, value: bears[index++] }
    }

    return { done: true, value: undefined }
  }
}
console.log(iterator.next()) //{ done: false, value: 'ice' }
console.log(iterator.next()) //{ done: false, value: 'panda' }
console.log(iterator.next()) //{ done: false, value: 'grizzly' }
console.log(iterator.next()) //{ done: true, value: undefined }
  • 这是一个实现了next方法的对象,next方法返回一个包含done属性和value属性的对象,且key的值类型为boolean或any,这符合迭代器协议,确认为一个标准的迭代器。
  • 弊端
    • 这种实现违背了高内聚原则,因为index和iterator对象本应属于一个整体,却使用了全局变量。从V8引擎的垃圾回收(GC)机制,特别是可达性分析(标记清除法)来看,如果bears = null,且未手动设置为null,极有可能导致内存泄漏,同时内聚性也较低。
    • 如果需要创建一百个迭代器对象,难道要手动定义一百次吗?显然这是错误的做法。我们需要将其封装起来,这样既能提高内聚性,又能实现代码复用,一举两得,真正实现了very beautiful,very 优雅。。

1.2 迭代器的封装实现

请思考以下代码:

const bears = ['ice', 'panda', 'grizzly']

function createArrIterator(arr) {
  let index = 0

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

      return { done: true, value: undefined }
    }
  }
  return _iterator
}
let iter = createArrIterator(bears)

console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
  • 该实现具有极高的内聚性,最大程度地进行了代码复用,减少了冗余。

2. 什么是可迭代对象

迭代器对象与可迭代对象是两个不同的概念,尽管它们之间存在关联。在面试中这些概念常被提及,此处不再赘述,直接进入主题。

  • 首先,它是一个符合可迭代对象协议(iterable protocol)的对象。
  • 可迭代对象协议

    它实现了以[Symbol.iterator]为键的方法,且该方法返回一个迭代器对象。

  • 经过一番梳理终于搞明白了概念,那么可迭代对象有什么好处?有什么应用场景呢?

    在使用for...of循环时,其本质就是调用这个以[Symbol.iterator]为键的方法。

2.1 原生可迭代对象(JavaScript内置)

  • String
  • Array
  • Set
  • NodeList 类数组对象
  • Arguments 类数组对象
  • Map

2.1.1 部分for...of演示

let str = 'The Three Bears'

const bears = ['ice', 'panda', 'grizzly']

for( let text of str) {
  console.log(text) //字符串每个遍历打印
}

for( let bear of bears) {
  console.log(bear)
}
 //ice panda grizzly

2.1.2 查看内置的[Symbol.iterator]方法

  • 上文列举了许多可迭代对象,它们必然符合可迭代对象协议。请思考以下代码:
const bears = ['ice', 'panda', 'grizzly']
//数组的Symbol.iterator方法
const iter = bears[Symbol.iterator]()

console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

const nickName = 'ice'
//字符串的Symbol.iterator方法
const strIter = nickName[Symbol.iterator]()

console.log(strIter.next())
console.log(strIter.next())
console.log(strIter.next())
console.log(strIter.next())

2.2 可迭代对象的实现

let info = {
  bears: ['ice', 'panda', 'grizzly'],
  [Symbol.iterator]: function() {
    let index = 0
    let _iterator = {
       //这里一定要箭头函数,或者手动保存上层作用域的this
       next: () => {
        if (index < this.bears.length) {
          return { done: false, value: this.bears[index++] }
        }
  
        return { done: true, value: undefined }
      }
    }

    return _iterator
  }
}

let iter = info[Symbol.iterator]()
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

//符合可迭代对象协议 就可以利用 for of 遍历
for (let bear of info) {
  console.log(bear)
}
//ice panda grizzly
  • 符合可迭代对象协议,是一个包含[Symbol.iterator]方法的对象,且该方法返回一个迭代器对象。
  • 当使用for...of进行遍历时,会自动调用此方法。

2.3 可迭代对象的应用

  • for...of
  • 展开语法
  • 解构语法
  • promise.all(iterable)
  • promise.race(iterable)
  • Array.from(iterable)
  • ...

2.4 自定义类迭代实现

class myInfo {
  constructor(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
  }

  [Symbol.iterator]() {
    let index = 0

    let _iterator = {
      next: () => {
        const friends = this.friends
        if (index < friends.length) {
          return {done: false, value: friends[index++]}
        }

        return {done: true, value: undefined}
      }
    }

    return _iterator
  }
}
const info = new myInfo('ice', 22, ['panda','grizzly'])

for (let bear of info) {
  console.log(bear)
}
//panda
//grizzly
  • 此案例仅对friends进行了迭代,你也可以迭代任何你想要的内容...
  • 请牢记此案例,后续我们将对此进行重构,其优雅程度将难以用言语形容。

3. 生成器函数

生成器是ES6新增的一种控制函数执行的方案,能够灵活地控制函数的暂停与继续执行。

生成器函数与普通函数的区别

  • 定义:普通函数使用function定义,而生成器函数使用function*,并在函数名后添加*。
  • 生成器函数可以通过yield来控制函数的执行流程。
  • 生成器函数返回一个生成器(generator),生成器是一种特殊的迭代器。

3.1 生成器函数基本实现

function* bar() {
  console.log('fn run')
}

bar()
  • 我们会发现,该函数并未立即执行。前文提到,它是一个生成器函数,其返回值是一个生成器,同时也是一种特殊的迭代器。与普通函数相比,它似乎处于暂停状态,那么如何使其执行呢?接下来我们将进一步探讨。

3.2 生成器函数单次执行

function* bar() {
  console.log('fn run')
}

const generator = bar()

console.log(generator.next())
//fn run
//{ value: undefined, done: true }
  • 返回了一个生成器,调用next方法即可让函数执行。next方法是有返回值的,正如前文在迭代器部分所探讨的,若value没有返回值,则为undefined。那么yield关键字在哪里?它究竟是如何控制函数的?又是如何使用呢?

3.3 生成器函数多次执行

function* bar() {
  console.log('fn run start')
  yield 100
  console.log('fn run...')
  yield 200
  console.log('fn run end')
  return 300
}

const generator = bar()

//1. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
console.log(generator.next())
//2. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
console.log(generator.next())
//3. 执行剩余代码
console.log(generator.next())

//打印结果:
//fn run start
//{done:false, value: 100}
//fn run...
//{done:false, value: 200}
//fn run end
//{done:true, value: 300}
  • 现在我们恍然大悟,每次调用next方法时,代码开始执行,直到遇到yield x后暂停,等待下一次调用next继续执行,周而复始。当不再存在yield关键字时,进行最后一次next调用,返回done:true。

3.4 生成器函数的分段传参

我有一个需求:既然生成器能控制函数分段执行,请实现一个分段传参的功能。

请思考以下代码:

function* bar(nickName) {
  const str1 = yield nickName
  const str2 = yield str1 + nickName
  return str2 + str1 + nickName
}
const generator = bar('ice')
console.log(generator.next())
console.log(generator.next('panda '))
console.log(generator.next('grizzly '))
console.log(generator.next())

// { value: 'ice', done: false }
// { value: 'panda ice', done: false }
// { value: 'grizzly panda ice', done: true }
// { value: undefined, done: true }
  • 如果没有接触过此类代码,可能会感到奇怪。
    • 当调用next函数时,yield左侧可以接收参数,但并非所有next方法的实参都能传递到生成器函数内部。
    • yield左侧接收的是第二次调用next时传入的实参。第一次传入的参数没有yield关键字接收,因此只有通过调用bar函数时传入。
    • 最后一次next调用传入的参数也无法被接收,因为没有yield关键字可供接收。
  • 许多开发者会疑惑,这样写有什么用?可读性似乎较差。但在处理异步数据时,这种方式非常有用,后续将在Promise相关文章中介绍。

3.5 生成器代替迭代器

前文提到,生成器是一种特殊的迭代器,因此生成器必定可以代替迭代器对象。请思考以下代码。

let bears = ['ice','panda','grizzly']

function* createArrIterator(bears) {
  for (let bear of bears) {
    yield bear
  }
}
const generator = createArrIterator(bears)
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())

此外,这里还有一种语法糖写法yield*。

  • yield* 依次迭代这个可迭代对象,相当于遍历并取出每一项 yield item(伪代码)。

请思考以下代码:

let bears = ['ice','panda','grizzly']

function* createArrIterator(bears) {
  yield* bears
}
const generator = createArrIterator(bears)
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
  • 依次迭代这个可迭代对象,返回每个item值。

4. 可迭代对象的终极封装

class myInfo {
  constructor(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
  }
  *[Symbol.iterator]() {
    yield* this.friends
  }
}
const info = new myInfo('ice', 22, ['panda','grizzly'])
for (let bear of info) {
  console.log(bear)
}

//panda
//grizzly
  • 回顾以下可迭代对象协议
    • 它是一个包含[Symbol.iterator]方法的对象。
    • 该方法返回一个迭代器对象。生成器函数返回一个生成器,生成器是一种特殊的迭代器。

5. 总结

5.1 迭代器对象

  • 本质是一个符合迭代器协议的对象。
  • 拥有对应的next方法,next方法返回一组数据{done:boolean, value:any}。

5.2 可迭代对象

  • 本质是一个符合可迭代对象协议的对象。
  • 拥有[Symbol.iterator]方法,调用该方法返回一个迭代器。

5.3 生成器函数

  • 可以控制函数的暂停执行和继续执行。
  • 通过function* bar() {}这种形式定义。
  • 不会立即执行,而是返回一个生成器,生成器是一种特殊的迭代器对象。
  • yield 关键字可以控制函数分段执行。
  • 调用返回生成器的next方法进行执行。

到此为止,关于JavaScript中的迭代器、可迭代对象与生成器的文章介绍完毕。如需了解更多JavaScript迭代器相关内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!

作者最新文章