精读《JavaScript 高级程序设计 第4版》第7章 迭代器与生成器
7.1 迭代器模式
7.1.1 可迭代协议
实现Iterator 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator 接口的对象的能力。在ECMAScript中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator作为键。该默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
ES6+ 中许多内置类型都默认实现了可迭代协议。
Array
String
Map
Set
arguments对象
NodeList等 DOM 集合类型
let num = 1;
let obj = {};
//这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]);//undefined
console.log(obj[Symbol.iterator]);//undefined
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
//这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]);//[Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]);//[Function: values]
console.log(map[Symbol.iterator]);//[Function: entries]
console.log(set[Symbol.iterator]);//[Function: values]
//调用迭代器工厂函数,返回迭代器对象
console.log(str[Symbol.iterator]());//Object [String Iterator] {}
console.log(arr[Symbol.iterator]());//Object [Array Iterator] {}
console.log(map[Symbol.iterator]());//[Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] }
console.log(set[Symbol.iterator]());//[Set Iterator] { 'a', 'b', 'c' }
接收可迭代对象的原生语言特性包括:
- for-of循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all()接收由期约组成的可迭代对象
- Promise.race()接收由期约组成的可迭代对象
- yield*操作符,在生成器中使用。
这些原生语言结构会在后台调用提供的可迭代对象的工厂函数,从而创建一个迭代器。如果对象原型链上的父类实现了Iterator 接口,那么该对象也就实现了该接口。
let arr = ['foo', 'bar', 'baz'];
// for...of 循环本质上会做:
// 1. 调用 arr[Symbol.iterator]() 获取迭代器
// 2. 反复调用迭代器的 .next() 方法
// 3. 将返回对象的 value 赋给变量,直到 done 为 true
for (let el of arr) {
console.log(el); // 'foo', 'bar', 'baz'
}
//数组解构
let [a,b,c] = arr;
console.log(a, b, c);// 'foo', 'bar', 'baz'
7.1.2 迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next()都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。
next() 方法返回一个包含以下两个属性的IteratorResult对象:
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next()都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。
value: 当前迭代的值。可以是任何类型。done: 一个布尔值。表示是否还可以再次调用next()会返回下一个值;
-
false表示迭代尚未完成,下次调用next()会返回下一个值。true表示迭代已结束。此时value属性可以省略,如果存在则被认为是迭代器的“返回值”,但for...of循环会忽略它。
let arr = ['foo', 'bar', 'baz']
console.log(arr[Symbol.iterator]);//[Function: values]
//创建两个迭代器
let iter = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();
console.log(iter.next());//{ value: 'foo', done: false }
console.log(iter.next());//{ value: 'bar', done: false }
console.log(iter.next());//{ value: 'baz', done: false }
//只要迭代器到达done:true,后续的next()调用都返回相同的值
console.log(iter.next());//{ value: undefined, done: true }
console.log(iter.next());//{ value: undefined, done: true }
//两个迭代器是相互独立的,只会独立的进行迭代
console.log(iter2.next());//{ value: 'foo', done: false }
arr.splice(1,0,'qux');
//如果迭代对象在迭代期间被修改了,迭代器也会反映相应的变化
console.log(iter2.next());//{ value: 'qux', done: false }
console.log(iter2.next());//{ value: 'bar', done: false }
注意:迭代器维护着一个指向可迭代对象的引用,因此迭代器会组织垃圾回收程序回收可迭代对象。
7.1.3 提前终止迭代器
可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。
提前关闭可能的情况包括:
- for-of循环通过break、continue、return或throw提前退出;
- 解构操作并未消费所有值
return()方法必须返回一个有效的IteratorResult对象,简单情况下可以只返回{done: true}。
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator](){
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { value: count++, done: false};
} else {
return { done: true };
}
},
return(){
console.log('Exiting early');
return {done: true};
}
};
}
}
let counter = new Counter(5);
for (let value of counter) {
console.log(value);
if (value === 2) {
break;
}
}
//1
//2
//Exiting early
let counter2 = new Counter(5);
try{
for (let value of counter2) {
console.log(value);
if (value === 2) {
throw 'error';
}
}
}catch(e){
}
//1
//2
//Exiting early
let [a,b,c] = new Counter(5);
//Exiting early
如果迭代器没有关闭,则还可以从上次离开的位置继续迭代。比如数组的迭代器就无法关闭:
let a = [1,2,3,4,5]
let iter = a[Symbol.iterator]()
for(let i of iter){
console.log(i)
if(i > 2){
break;
}
}
//1
//2
//3
for(let i of iter){
console.log(i)
}
//4
//5
因为return()方法是可选的,所有并非所有迭代器都是可以关闭的。要知道某个迭代器是不是可以关闭的,可以测试该迭代器实例的return属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加一个return()方法并不能使其变成可关闭的。但是return()方法会被调用,但无法强制关闭。
7.2 生成器
生成器是ECMAScript6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
7.2.1 生成器基础
生成器的形式是一个函数,函数名称前加一个星号(*)表示它是一个生成器,标识生成器函数的星号不受两侧空格的影响。只要是可以定义函数的地方就可以定义生成器,但是箭头函数不能用来定义生成器函数。
//生成器函数声明
function* generatorFn(){}
//作为函数调用表达式的生成器函数
let generatorFn1 = function*(){}
//作为对象属性的生成器函数
let foo = {
*generatorFn2(){}
}
//作为类实例方法的生成器函数
class Foo{
*generatorFn3(){}
}
//作为静态方法的生成器函数
class Bar {
static *generatorFn4(){}
}
调用生成器函数并不会立即执行其函数体,而是返回一个生成器对象。这个生成器对象既符合可迭代协议(拥有 [Symbol.iterator] 方法),也符合迭代器协议(拥有 next() 方法)。生成器对象一开始处于暂停状态(suspended),通过next()方法可以让生成器开始或恢复执行。
function* generatorFn(){}
let generatorObj = generatorFn();
console.log(generatorObj);//Object [Generator] {}
console.log(generatorFn()[Symbol.iterator]);//[Function: [Symbol.iterator]]
//生成器对象实现了Iterator接口,它们默认的迭代器是自引用的。
console.log(generatorObj === generatorObj[Symbol.iterator]());//true
console.log(generatorObj.next());//{ value: undefined, done: true }
7.2.2 通过yield中断执行
yield 关键字:在生成器函数内部使用。它会暂停函数的执行,并将紧跟在 yield 后面的值作为 next() 返回对象的 value。下一次调用 next() 时,函数会从上次暂停的地方继续执行。通过yield关键字退出的生成器函数会处于done:false状态,通过return关键字退出的生成器函数会处于done:true状态。生成器函数内部的执行流程会针对每个生成器对象区分作用域,相互直接不影响。
function* generatorFn() {
yield 'foo';
yield 'bar';
return 'baz'; // return 的值会在 done 为 true 时作为 value
}
const generatorObject = generatorFn();
console.log(generatorObject.next()); // { value: 'foo', done: false }
console.log(generatorObject.next()); // { value: 'bar', done: false }
console.log(generatorObject.next()); // { value: 'baz', done: true }
// 因为生成器对象也是可迭代的,所以可以直接用 for...of
for (const item of generatorFn()) {
console.log(item); // 'foo', 'bar' (注意:for...of 不会消费 'baz',因为遇到 done: true 就停止了)
}
生成器的控制权交换:
yield 本质上是双向的。它不仅能把值传出去,还能通过 next(val) 方法接收外部传入的值,这使得生成器可以用来实现复杂的协程(Coroutine)模式。
function* generatorFn(){
return yield 'foo';
}
let generatorObj = generatorFn();
console.log(generatorObj.next());//{value: 'foo', done: false}
console.log(generatorObj.next('bar'));//{value: 'bar', done: true}
function* nTimes(n){
let i = 0;
while(n--){
yield i++;
}
}
for(let x of nTimes(3)){
console.log(x);
}
//0
//1
//2
//实现一个range函数,返回一个序列,从start开始到end结束,每次增加step,默认step为1
function* range(start, end){
while(start < end){
yield start++;
}
}
for(let x of range(4, 7)){
console.log(x);
}
//4
//5
//6
产生可迭代对象:
可以使用星号增强yield行为,让它能够迭代一个可迭代对象,从而一次只产出一个值。
function* generatorFn() {
yield* [1, 2, 3];
}
for (const value of generatorFn()) {
console.log(value);
}
//1
//2
//3
使用yield*实现递归算法:
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n-1;
}
}
for (const value of nTimes(3)) {
console.log(value);
}
//0
//1
//2
生成器作为默认迭代器
class Foo {
constructor(){
this.values = [1,2,3]
}
*[Symbol.iterator]() {
yield* this.values;
}
}
const foo = new Foo();
for (const value of foo) {
console.log(value);
}
//1
//2
//3
7.2.3 提前终止生成器
生成器除了next()方法,还有一个可选的return()方法和throw()方法。都可以用于强制生成器进入关闭状态。
1、return()
该方法强制生成器进入关闭状态。提供给return()的值就是终止迭代器对象的值。与迭代器不同,所有生成器对象都有return()方法,一旦通过它进入关闭状态就无法恢复。
function* generatorFn(){
yield* [1,2,3];
}
const g = generatorFn();
console.log(g.next());//{value:1,done:false}
console.log(g.return(4));//{value:4,done:true}
console.log(g.next());//{value:undefined,done:true}
2、throw()
该方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器会关闭。
function* generatorFn(){
for(let i=0;i<3;i++){
yield i;
}
}
const g = generatorFn();
console.log(g.next());//{ value: 0, done: false }
try{
g.throw('foo');
}catch(e){
console.log(e);//foo
}
console.log(g.next());//{ value: undefined, done: true }
不过如果生成器函数内部处理了这个错误,那么生成器就不会关闭,可以恢复执行。但是错误处理会跳过对应的yield。
function* generatorFn(){
for(let i=0;i<3;i++){
try{
yield i;
}catch(e){
console.log(e);
}
}
}
const g = generatorFn();
console.log(g.next());//{ value: 0, done: false }
g.throw('foo');//foo
console.log(g.next());//{ value: 2, done: false }
7.3 异步迭代
ES2018 引入了异步迭代器和 for-await-of 循环,用于处理异步数据流。
1. 异步迭代协议
它与同步迭代协议类似,但有两点关键区别:
- 可迭代协议:对象实现
[Symbol.asyncIterator]方法(而不是[Symbol.iterator])。 - 迭代器协议:该迭代器的
next()方法返回一个 Promise。这个 Promise 解决后,其值是一个形如{ value, done }的对象。
2. for-await-of 循环
这个循环专门用于消费异步可迭代对象。它会在每次迭代中 await 迭代器 next() 方法返回的 Promise。
// 假设我们有一个函数,它分页获取数据,每一页是一个异步请求
async function* asyncPaginatedFetcher() {
let page = 1;
while (true) {
// `fetch` 返回一个 Promise
const response = await fetch(`/api/data?page=${page}`);
const data = await response.json();
if (data.length === 0) {
break; // 没有更多数据了
}
for (const item of data) {
// 逐个产出数据项,但每次 next() 调用都是异步的
yield item;
}
page++;
}
}
// 使用 for-await-of 消费这个异步生成器
(async () => {
for await (const dataItem of asyncPaginatedFetcher()) {
console.log(dataItem);
// 循环会自动等待每一页数据的获取和每一个项的产出
// 实现了以同步写法处理异步数据流
}
})();
3. 应用场景
- 分页获取所有数据:如上例所示。
- 处理 Node.js 流:Node.js 的
readable流在objectMode或使用stream.values()后,可以作为异步可迭代对象使用。 - WebSocket 消息:可以创建一个异步生成器来持续接收 WebSocket 消息。
- RxJS Observables:虽然 Observables 本身不是异步可迭代对象,但可以很容易地适配。
7.4 总结
|
概念 |
核心思想 |
现代应用 |
|
迭代器协议 |
定义了如何产生一个值序列( 方法)。 |
所有集合操作、解构、扩展运算符的基础。 |
|
可迭代协议 |
定义了如何获取一个迭代器( 方法)。 |
让自定义数据结构(如链表、树)能够与 JavaScript 原生语法( , )无缝集成。 |
|
生成器 |
通过 和 简化迭代器的创建。提供了暂停/恢复的能力。 |
Redux Saga 等库用其管理复杂异步流;实现无限序列;协程。 |
|
异步迭代 |
迭代器 返回 Promise,用于处理异步数据流。 |
消费分页 API、Node.js 流、实时消息流( )。 |
更多推荐



所有评论(0)