JavaScript 迭代器、可迭代对象与生成器详解
本文详细解析 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 grizzly2.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迭代器相关内容,请搜索本站以前的文章或继续浏览下面的相关文章。希望大家以后多多支持本站!
JS中使用位运算(~~)和一元加号(+)快速转换字符串为整数
2026-11-03 22:41:12
怎么使用命令行批量格式化项目中的JS文件
2026-11-03 19:04:42
JS代码美化后报错怎么办?常见格式化冲突解决方法
2026-11-03 18:47:01
不用moment.js,怎么用原生JS Intl API格式化时间戳
2026-11-03 18:10:30
原生JS与dayjs获取当前日期的代码对比
2026-11-03 17:57:26
