第7章 迭代器与生成器

本文详细解释了迭代机制、循环与迭代器模式在JavaScript中的作用,介绍了ES6中的迭代器和生成器特性,包括它们如何工作、如何使用for-of循环、yield关键字以及生成器在实现递归和自定义迭代器中的应用。
摘要由CSDN通过智能技术生成

1 理解迭代

循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循
环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。

let collection = ['foo', 'bar', 'baz']; 
for (let index = 0; index < collection.length; ++index) { 
 console.log(collection[index]); 
}

通过这种循环来执行例程并不理想,因为:

  1. 迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,
    然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。
  2. 遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适 用于其他具有隐式顺序的数据结构。

ES5 新增 Array.prototype.forEach()方法,解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组。解决方案ES6支持的迭代器模式

2 迭代器模式

迭代器模式:实现了正式的 Iterable 接口,可以把可迭代对象理解成数组或集合这样的集合类型的对
象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

// 数组的元素是有限的
// 递增索引可以按序访问每个元素
let arr = [3, 1, 4]; 
// 集合的元素是有限的
// 可以按插入顺序访问每个元素
let set = new Set().add(3).add(1).add(4);

可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。

for (let i = 1; i <= 10; ++i) { 
 console.log(i); 
}

2.1 可迭代协议

这个默认迭代器属性必须引用一个迭代器工厂
函数,Symbol.iterator作为键,很多内置类型都实现了 Iterable 接口:

  • 字符串数组
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等 DOM 集合类型

检查是否存在默认迭代器属性可以暴露这个工厂函数:

let obj = {};
console.log(obj[Symbol.iterator]); // undefined

接收可迭代对象的原生语言特性包括:
 for-of 循环;
 数组解构 : let [a, b, c] = arr;
 扩展操作符:let arr2 = […arr];
 Array.from()
 创建集合:new Set(arr);
 创建映射
 Promise.all()接收由期约组成的可迭代对象
 Promise.race()接收由期约组成的可迭代对象
 yield*操作符,在生成器中使用

2.2 迭代器协议

迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象包含两个属性:done 和 value。

// 可迭代对象
let arr = ['foo', 'bar'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator](); 
console.log(iter); // ArrayIterator {}
// 执行迭代
console.log(iter.next()); // { done: false, value: 'foo' } 
console.log(iter.next()); // { done: false, value: 'bar' } 
console.log(iter.next()); // { done: true, value: undefined }

注意 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可
迭代对象。

2.3 自定义迭代器

让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,
可以把计数器变量放到闭包里,然后通过闭包返回迭代器

class Counter { 
	 constructor(limit) { 
	 	this.limit = limit; 
	 } 
	 [Symbol.iterator]() { 
		 let count = 1, 
		 limit = this.limit; 
		 return { 
			 next() { 
				 if (count <= limit) { 
				 	return { done: false, value: count++ }; 
				 } else { 
				 	return { done: true, value: undefined }; 
				 } 
			 } 
	 }; 
	 } 
} 
let counter = new Counter(3); 
for (let i of counter) { console.log(i); } 
// 1 
// 2 
// 3

2.4 提前终止迭代器

可选的 return()方法用于指定在迭代器提前关闭时执行的逻辑。for-of 循环通过 break、continue、return 或 throw 提前退出。

3 生成器

生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的
能力。

3.1 生成器基础

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义
函数的地方,就可以定义生成器。

// 生成器函数声明
function* generatorFn() {} 
// 生成器函数表达式
let generatorFn = function* () {} 
// 作为对象字面量方法的生成器函数
let foo = { 
 * generatorFn() {} 
} 
// 作为类实例方法的生成器函数
class Foo { 
 * generatorFn() {} 
} 
// 作为类静态方法的生成器函数
class Bar { 
 static * generatorFn() {} 
}

注意 箭头函数不能用来定义生成器函数。

生成器对象一开始处于暂停执行(suspended)的状态。生成器对象也实现了 Iterator 接口,因此具有 next()方法。调用这个方法会让生成器开始或恢复执行。

function* generatorFn() {} 
const g = generatorFn(); 
console.log(g); // generatorFn {<suspended>} 
console.log(g.next); // f next() { [native code] }

如上,因为函数体为空的生成器函数中间不会停留,调用一次 next()就会让生成器到达 done: true 状态。

console.log(g.next()); // { done: true, value: undefined }

value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:

function* generatorFn() { 
 return 'foo'; 
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: true, value: 'foo' }

3.2 通过yield中断执行

yield 关键字可以让生成器停止,next()方法恢复执行:

function* generatorFn() { 
 yield; 
} 
let generatorObject = generatorFn(); 
console.log(generatorObject.next()); // { done: false, value: undefined } 
console.log(generatorObject.next()); // { done: true, value: undefined }

yield生成的值会出现在 next()方法返回的对象里。yield 关键字退出生成器函数会处在 done: false 状态;通过 return 关键字退出的生成器函数会处于 done: true 状态。

function* generatorFn() { 
 yield 'foo'; 
 yield 'bar'; 
 return 'baz'; 
} 
let generatorObject = generatorFn(); 
console.log(generatorObject.next()); // { done: false, value: 'foo' } 
console.log(generatorObject.next()); // { done: false, value: 'bar' } 
console.log(generatorObject.next()); // { done: true, value: 'baz' }

生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用 next()
不会影响其他生成器:

function* generatorFn() { 
 yield 'foo'; 
 yield 'bar'; 
 return 'baz'; 
} 
let generatorObject1 = generatorFn(); 
let generatorObject2 = generatorFn(); 
console.log(generatorObject1.next()); // { done: false, value: 'foo' } 
console.log(generatorObject2.next()); // { done: false, value: 'foo' }

yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return 关键字:

// 有效
function* validGeneratorFn() { 
	yield; 
} 
// 无效
function* invalidGeneratorFnA() { 
 function a() { 
 	yield; 
 } 
}

1.生成器对象作为可迭代对象

把生成器对象当成可迭代对象:

function* generatorFn() { 
 yield 1; 
 yield 2; 
 yield 3; 
} 
for (const x of generatorFn()) { 
 console.log(x); 
} 
// 1 
// 2 
// 3

在需要自定义迭代对象时,这样使用生成器对象会特别有用:

function* nTimes(n) { 
 while(n--) { 
 yield; 
 } 
}
for (let _ of nTimes(3)) { 
 console.log('foo'); 
} 
// foo 
// foo 
// foo

2.使用 yield 实现输入和输出

yield 关键字还可以作为函数的中间参数使用,让生成器函数暂停的 yield 关键字会接收到传给 下个next()方法的值。第一次调用 next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数:

function* generatorFn(initial) { 
 console.log(initial); 
 console.log(yield); 
 console.log(yield); 
} 
let generatorObject = generatorFn('foo'); 
generatorObject.next('bar'); // foo 
generatorObject.next('baz'); // baz 
generatorObject.next('qux'); // qux

yield 关键字可以同时用于输入和输出,如下例所示:

function* generatorFn() { 
 return yield 'foo'; 
} 
let generatorObject = generatorFn(); 
console.log(generatorObject.next()); // { done: false, value: 'foo' } 
console.log(generatorObject.next('bar')); // { done: true, value: 'bar' }

因为函数必须对整个表达式求值才能确定要返回的值,return yield 暂停执行并计算要产生的值:“foo”,下一次调用 next()传入了"bar",作为交给同一个 yield 的值。然后这个值被确定为本次生成器函数要返回的值。

3.产生可迭代对象

使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值:

// 等价的 generatorFn: 
// function* generatorFn() { 
// for (const x of [1, 2, 3]) { 
// yield x; 
// } 
// } 
function* generatorFn() { 
 yield* [1, 2, 3]; 
} 
let generatorObject = generatorFn(); 
for (const x of generatorFn()) { 
 console.log(x); 
} 
// 1 
// 2 
// 3

yield*的值是关联迭代器返回 done: true 时的 value 属性。对于普通迭代器来说,这个值是undefined:

function* generatorFn() { 
 console.log('iter value:', yield* [1, 2, 3]); 
} 
for (const x of generatorFn()) { 
 console.log('value:', x); 
} 
// value: 1 
// value: 2 
// value: 3 
// iter value: undefined

对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值:

function* innerGeneratorFn() { 
 yield 'foo'; 
 return 'bar'; 
} 
function* outerGeneratorFn(genObj) { 
 console.log('iter value:', yield* innerGeneratorFn()); 
} 
for (const x of outerGeneratorFn()) { 
 console.log('value:', x); 
} 
// value: foo 
// iter value: bar

4.使用yield*实现递归算法

yield*最有用的地方是实现递归操作

3.3 生成器作为默认迭代器

class Foo { 
 constructor() { 
 this.values = [1, 2, 3]; 
 }
  * [Symbol.iterator]() { 
 yield* this.values; 
 } 
} 
const f = new Foo(); 
for (const x of f) { 
 console.log(x); 
} 
// 1 
// 2 
// 3

这里,for-of 循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。
这个生成器对象是可迭代的,所以完全可以在迭代中使用。

3.4 提前终止生成器

与迭代器类似,生成器也支持“可关闭”的概念。一个实现 Iterator 接口的对象一定有 next()方法,还有一个可选的 return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。

function* generatorFn() { 
 for (const x of [1, 2, 3]) { 
 yield x; 
 } 
}
const g = generatorFn(); 
console.log(g); // generatorFn {<suspended>} 
console.log(g.return(4)); // { done: true, value: 4 } 
console.log(g); // generatorFn {<closed>}

与迭代器不同,所有生成器对象都有 return()方法,只要通过它进入关闭状态,就无法恢复了。
后续调用 next()会显示 done: true 状态

throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭,处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield。

4 小结

ECMAScript 6 正式支持迭代模式并引入了两个新的语言特性:迭代器和生成器。

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next()方法接收输入和产生输出。在加上星号之后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值