【JavaScript由浅入深】细说生成器、迭代器
✨前言
处理集合中的每个项是很常见的操作。JavaScript 提供了许多迭代集合的方法,从简单的
for
循环到map()
和filter()
。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义
for...of
循环的行为。
一、迭代器
1.1 什么是迭代器?
- 迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
- 从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
- 在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
- 在JavaScript中这个标准就是一个特定的next方法;
- next方法有如下的要求:
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
- done(boolean)
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value
- 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
- 一旦创建,迭代器对象可以通过重复调用 next()显式地迭代。迭代一个迭代器被称为消耗了这个迭代器,因为它通常只能执行一次。在产生终止值之后,对 next()的额外调用应该继续返回{done:true}
🌰迭代器练习:
const students = ["lisa", "mike", "beak"]
let index = 0
const studentsIterator = {
next: function() {
if(index < students.length) {
return {done: false, value: students[index++] }
} else {
return { done: true, value: undefined }
}
}
}
console.log(studentsIterator.next())
console.log(studentsIterator.next())
console.log(studentsIterator.next())
console.log(studentsIterator.next())
输出如下:
1.2 可迭代对象
上述代码的弊端:我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
-
若一个对象拥有迭代行为,比如在
for...of
中会循环哪些值,那么那个对象便是一个可迭代对象。一些内置类型,如Array
或Map
拥有默认的迭代行为,而其他类型(比如Object
)则没有。 -
为了实现可迭代,一个对象必须实现 @@iterator 方法,这意味着这个对象(或其原型链中的任意一个对象)必须具有一个带
Symbol.iterator
键(key)的属性。 -
可以多次迭代一个迭代器,或者只迭代一次。
-
只能迭代一次的 Iterables(例如 Generators)通常从它们的**@@iterator方法中返回它本身,其中那些可以多次迭代的方法必须在每次调用@@iterator**时返回一个新的迭代器。
举个栗子:将Object
变成可迭代对象
// 将info变成可迭代对象
/*
1.必须实现一个特定的函数: [Symbol.iterator]
2.这个函数需要返回一个迭代器(这个迭代器用于迭代当前的对象)
*/
const info = {
students: ["lisa", "beak", "chen"],
[Symbol.iterator]: function () {
let index = 0
const infoIterator = {
next: function () {
if (index < info.students.length) {
return { done: false, value: this.students[index++] }
} else {
return { done: true, value: undefined }
}
}
}
return infoIterator
}
}
// 给info创建一个迭代器, 迭代info中的students
console.log(infoIterator.next())
console.log(infoIterator.next())
console.log(infoIterator.next())
console.log(infoIterator.next())
// 可迭代对象必然具备下面的特点
const iterator = infos[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// 可迭对象可以进行for of操作
for (const item of infos) {
console.log(item)
}
1.3 可迭代对象的应用
应用场景:
- JavaScript中语法:
for ...of
、展开语法(spread syntax)、yield*
、解构赋值(Destructuring_assignment); - 创建一些对象时:
new Map([Iterable])
、new WeakMap([iterable])
、new Set([iterable])
、new WeakSet([iterable])
; - 一些方法的调用:
Promise.all(iterable)
、Promise.race(iterable)
、Array.from(iterable)
二、生成器
虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。
生成器函数提供了一个强大的选择:
- 它允许你定义一个包含自有迭代算法的函数
- 同时它可以自动维护自己的状态
- 生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
- 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
- 生成器函数也是一个函数,但是和普通的函数有一些区别:
- 首先,生成器函数需要在function的后面加一个符号:*
- 其次,生成器函数可以通过yield关键字来控制函数的执行流程:
- 最后,生成器函数的返回值是一个Generator(生成器):
- 生成器事实上是一种特殊的迭代器;
举个栗子:
// 1.定义了一个生成器函数
function* fn() {
console.log("1")
console.log("2")
yield
console.log("3")
console.log("4")
yield
console.log("5")
console.log("6")
}
// 2.调用生成器函数, 返回一个 生成器对象
const generator = fn()
// 调用next方法
generator.next()
generator.next()
generator.next()
既然生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
const students = ["lisa", "mike", "beak"]
function* arrayIterator(arr) {
for (const item of arr) {
yield item
}
}
const studentsIterator = arrayIterator(students)
console.log(studentsIterator.next())
console.log(studentsIterator.next())
console.log(studentsIterator.next())
console.log(studentsIterator.next())