迭代器和生成器是 ES6 中引入的特性。迭代器通过一次消费一个项目列表来提高效率,类似于数据流。生成器是一种能够暂停执行的特殊函数。调用生成器允许以块的形式(一次一个)生成数据,而无需先将其存储在列表中。下面就来深入理解 JavaScript 中的迭代器和生成器,看看它们是如何使用的,又有何妙用!
迭代器
JavaScript 中的迭代器可以分别两种:同步迭代器和异步迭代器。
1. 同步迭代器
(1)迭代器和可迭代对象
在 JavaScript 中有很多方法可以遍历数据结构。例如,使用 for
循环或使用 while
循环。迭代器具有类似的功能,但有显着差异。
迭代器只需要知道集合中的当前位置,而其他循环则需要预先加载整个集合才能循环遍历它。迭代器使用 next()
方法访问集合中的下一个元素。 但是,为了使用迭代器,值或数据结构应该是可迭代的。 数组、字符串、映射、集合是 JavaScript 中的可迭代对象。普通对象是不可迭代的。
(2)定义迭代器
下面来看看集合不可迭代的场景:
javascript
复制代码
const favouriteMovies = {
a: '哈利波特', b: '指环王', c: '尖峰时刻', d: '星际穿越', e: '速度与激情',}
这个对象是不可迭代的。如果使用普通的 for 循环遍历它,就会抛出错误。 随着 ES6 中迭代器的引入,可以将其转换为可迭代对象以便遍历它。 这些称为自定义迭代器。 下面看看如何实现对象的遍历并打印出来:
javascript
复制代码
favouriteMovies[Symbol.iterator] = function() { const ordered = Object.values(this).sort((a, b) => a - b); let i = 0; return { next: () => ({ done: i >= ordered.length, value: ordered[i++] }) }}for (const v of favouriteMovies) {
console.log(v);}
输出结果如下:
javascript
复制代码
哈利波特指环王尖峰时刻星际穿越速度与激情
这里使用 Symbol.iterator()
来定义迭代器。任何具有 Symbol.iterator
键的结构都是可迭代的。
可迭代对象具有以下行为:
- 当
for..of
循环开始时,它首先查找错误。如果未找到,则它会访问方法和定义该方法的对象。 - 以
for..of
循环方式迭代该对象。 - 使用该输出对象的
next()
方法来获取要返回的下一个值。 - 返回的值的格式为
done:boolean
,value: any
。 返回done:true
时循环结束。
下面来创建一个 LeapYear 对象,该对象返回范围为 (start, end) 的闰年列表,并在后续闰年之间设置间隔。
javascript
复制代码
class LeapYear { constructor(start = 2020, end = 2040, interval = 4) { this.start = start; this.end = end; this.interval = interval; } [Symbol.iterator]() { let nextLeapYear = this.start; return { next: () => { if (nextLeapYear <= this.end) { let result = { value: nextLeapYear, done: false }; nextLeapYear += this.interval; return result; } return { value: undefined, done: true }; }, }; } }
在上面的代码中,为自定义类型 LeapYear 实现了 Symbol.iterator() 方法。分别在 this.start
和 this.end
字段中有迭代的起点和终点。 使用 this.interval
来跟踪迭代的第一个元素和下一个元素之间的间隔。
现在,可以在自定义类型上调用 for...of
循环,并查看其行为和输出值,就像默认数组类型一样:
javascript
复制代码
let leapYears = new LeapYear(); for (const leapYear of leapYears) { console.log(leapYear); }
输出结果如下:
javascript
复制代码
202020242028203220362040
这里的 LeapYear
通过 Symbol.iterator(
) 变成了可迭代对象。
在一些情况下,迭代器会比普通迭代更好。 例如,在没有随机访问的有序集合(如数组)中,迭代器的性能会更好,因为它可以直接根据当前位置检索元素。但是,对于无序集合,由于没有顺序,就不会体验到性能上的重大差异。
使用普通循环算法,例如 for
循环或 while
循环,您只能循环遍历允许迭代的集合:
javascript
复制代码
const favourtieMovies = [ '哈利波特', '指环王', '尖峰时刻', '星际穿越', '速度与激情', ]; for (let i=0; i < favourtieMovies.length; i++) { console.log(favouriteMovies[i]); } let i = 0; while (i < favourtieMovies.length) { console.log(favourtieMovies[i]); i++; }
由于数组是可迭代的,因此可以使用 for 循环遍历。 我们也可以为上面实现一个迭代器,这将允许更好地访问基于当前位置的元素,而无需加载整个集合。代码如下:
javascript
复制代码
const iterator = favourtieMovies[Symbol.iterator](); iterator.next(); // { value: '哈利波特', done: false } iterator.next(); // { value: '指环王', done: false } iterator.next(); // { value: '尖峰时刻', done: false } iterator.next(); // { value: '星际穿越', done: false } iterator.next(); // { value: '速度与激情', done: false } iterator.next(); // { value: undefined, done: true }
next() 方法将返回迭代器的结果。它包括两个值; 集合中的元素和完成状态。 可以看到,当遍历完成后,即使访问数组外的元素,也不会抛出错误。 它只会返回一个具有 undefined
值和完成状态为 true
的对象。
(3)使用场景
那为什么向自定义对象中添加迭代器呢?我们也可以编写自定义函数来遍历对象以完成同样的事情。
实际上,迭代器是一种标准化自定义对象的优雅实现方式,它为自定义数据结构提供了一种在更大的 JS 环境中很好地工作的方法。因此,提供自定义数据结构的库经常会使用迭代器。例如, Immutable.JS 库就使用迭代器为其自定义对象(如Map)。所以,如果需要为封装良好的自定义数据结构提供原生迭代功能,就考虑使用迭代器。
2. 异步迭代器
JavaScript 中的异步迭代对象是实现 Symbol.asyncIterator
的对象:
javascript
复制代码
const asyncIterable = { [Symbol.asyncIterator]: function() { } };
我们可以将一个函数分配给 [Symbol.asyncIterator]
以返回一个迭代器对象。迭代器对象应符合带有 next()
方法的迭代器协议(类似于同步迭代器)。
下面来添加迭代器:
javascript
复制代码
const asyncIterable = { [Symbol.asyncIterator]: function() { let count = 0; return { next() { count++; if (count <= 3) { return Promise.resolve({ value: count, done: false }); } return Promise.resolve({ value: count, done: true }); } }; } };
这里用 Promise.resolve
包装了返回的对象。下面来执行 next()
方法:
javascript
复制代码
const go = asyncIterable[Symbol.asyncIterator]();
go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));
输出结果如下:
javascript
复制代码
12
也可以使用 for await...of
来对异步迭代对象进行迭代:
javascript
复制代码
asyncfunctionconsumer() {
forawait (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();
异步迭代器和迭代器是异步生成器的基础,后面会介绍异步生成器。
生成器
JavaScript 中的生成器可以分别两种:同步生成器和异步生成器。
1. 同步生成器
(1)基本概念
生成器是一个可以暂停和恢复并可以产生多个值的过程。JavaScript 中的生成器由一个生成器函数组成,它返回一个可迭代 Generator 对象。
生成器是对 JavaScript 的强大补充。它们可以维护状态,提供一种制作迭代器的有效方法,并且能够处理无限数据流,可用于在前端实现无限滚动等。此外,当与 Promises 一起使用时,生成器可以模拟 async/await 功能,这使我们能够以更直接和可读的方式处理异步代码。尽管 async/await 是处理常见、简单的异步用例(例如从 API 获取数据)的一种更普遍的方式,但生成器具有更高级的功能。
生成器函数是返回生成器对象的函数,由 function
关键字后面跟星号 (*) 定义,如下所示:
javascript
复制代码
function* generatorFunction() {}
有时,我们可能会在函数名称旁边看到星号,而不是 function
关键字,例如 function *generatorFunction()
,它的工作原理是相同的,但 function*
是一种更广泛接受的语法。
生成器函数也可以在表达式中定义,就像常规函数一样:
javascript
复制代码
const generatorFunction = function* () {}
生成器甚至可以是对象或类的方法:
javascript
复制代码
// 生成器作为对象的方法 const generatorObj = { *generatorMethod() {}, } // 生成器作为类的方法 class GeneratorClass { *generatorMethod() {} }
下面的例子都将使用生成器函数声明得语法。
注意:与常规函数不同,生成器不能使用 new 关键字构造,也不能与箭头函数结合使用。
现在我们知道了如何声明生成器函数,下面来看看生成器返回的可迭代生成器对象。
(2)生成器对象
传统的 JavaScript 函数会在遇到return
关键字时返回一个值。 如果省略 return
关键字,函数将隐式返回 undefined
。
例如,在下面的代码中,我们声明了一个 sum()
函数,它返回一个值,该值是两个整数参数的和:
javascript
复制代码
functionsum(a, b) {
return a + b}
调用该函数会返回一个值,该值是参数的总和:
javascript
复制代码
const value = sum(5, 6) // 11
而生成器函数不会立即返回值,而是返回一个可迭代的生成器对象。 在下面的例子中,我们声明了一个函数并给它一个单一的返回值,就像一个标准的函数:
javascript
复制代码
function* generatorFunction() {
return'Hello, Generator!'}
当调用生成器函数时,它将返回生成器对象,我们可以将其分配给一个变量:
javascript
复制代码
const generator = generatorFunction()
如果这是一个常规函数,我们希望生成器为我们提供函数中返回的字符串。 然而,我们实际得到的是一个处于挂起状态的对象。 因此,调用生成器将提供类似于以下内容的输出:
javascript
复制代码
generatorFunction {<suspended>} [[GeneratorLocation]]: VM335:1 [[Prototype]]: Generator [[GeneratorState]]: "suspended" [[GeneratorFunction]]: ƒ* generatorFunction() [[GeneratorReceiver]]: Window
函数返回的生成器对象是一个迭代器。迭代器是一个具有可用的 next()
方法的对象,该方法用于迭代一系列值。 next()
方法返回一个对象,其包含两个属性:
value
:当前步骤的值;done
:布尔值,指示生成器中是否有更多值。
next()
方法必须遵循以下规则:
- 返回一个带有
done: false
的对象来继续迭代; - 返回一个带有
done: true
的对象来停止迭代。
下面就来在生成器上调用 next()
并获取迭代器的当前值和状态:
javascript
复制代码
generator.next()
这将得到以下输出结果:
javascript
复制代码
{value: "Hello, Generator!", done: true}
调用 next()
时的返回值为 Hello, Generator!
,并且 done
的状态为 true
,因为该值来自关闭迭代器的返回值。 由于迭代器完成,生成器函数的状态将从挂起变为关闭。这时再次调用生成器将输出以下内容:
javascript
复制代码
generatorFunction {}
除此之外,生成器函数也有区别于普通函数的独特特征。下面我们就来了解一下 yield
运算符并看看生成器如何暂停和恢复执行。
掌握JavaScript中的迭代器和生成器(下)https://developer.aliyun.com/article/1411467