1. 迭代器和生成器
- 内容纪要
- 迭代
- 迭代器模式
- 生成器
1.1 理解迭代
- 在JavaScript中,计数循环就是一种最简单的迭代:
for (let i = 0; i < 10; i++) {
console.log(i);
}
1. 循环时迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
2. 迭代会在一个有序集合上进行。
const collection = ['foo','bar','baz']
for (let index = 0; index < collection.length; index++) {
console.log(collection[index]);
}
3. 因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。
4. 问题:`迭代之前需要事先知道如何使用数据结构。`数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况不适用于所有数据结构。
5. 遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。
6. ES5新增了Array.prototype.forEach()方法,向通用迭代需求买进了一步(但是仍然不够理想)
const collection = ['foo','bar','baz']
collection.forEach((item)=>{
console.log(item);
})
7. 这个方法解决了单独记录索引和通过数组对象取得值的问题,不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。
1.2 迭代器模式
迭代器模式描述了一个方案,即可以把有些结构称为"可迭代对象(iterable)",因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。
- 可迭代对象是一种抽象的语法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
- 任何实现Iterable接口的数据结构都可以被实现Iterator接口的结构消费。迭代器是按需创建的一次性对象。每个迭代器都关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。
- 迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。
1.2.1 可迭代协议
- 实现Iterable接口要求同时具备两种能力:支持迭代的自我辨识能力和创建实现Iterator接口的对象的能力。这意味着必须暴露一个属性作为"默认迭代器",而且这个属性必须使用特殊的Symbol.iterator作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
- 很多内置类型都实现了Iterable接口
- 字符串
- 数组
- 映射
- 集合
- arguments对象
- NodeList的DOM集合
- 检查是否存在默认迭代器属性可以暴露这个工厂函数
let num = 1
let obj = {}
console.log(num[Symbol.iterator]);
console.log(obj[Symbol.iterator]);
let str = 'abc'
let arr = ['1','2','3']
let m = new Map().set("val1","key1").set("val2","key2")
let s = new Set().add(1).add(2).add(3)
console.log(str[Symbol.iterator]);
console.log(arr[Symbol.iterator]);
console.log(m[Symbol.iterator]);
console.log(s[Symbol.iterator]);
console.log(str[Symbol.iterator]());
console.log(arr[Symbol.iterator]());
console.log(m[Symbol.iterator]());
console.log(s[Symbol.iterator]());
- 在实际的开发中,不需要显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。
- for-of循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.All()接收期约组成的可迭代对象
- Promise.race()接收由期约组成的可迭代对象
- yield* 操作符
class fooArray extends Array {}
let arr = new fooArray('foo','zoo')
for (const iterator of arr) {
console.log(iterator);
}
1.2.2 迭代器协议
- 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next(),则无法知道迭代器的当前位置。
- next()方法返回的迭代器对象IteratorResult包含两个属性:done和value。done是一个布尔值,表示是否还可以再次调用next()取得下一个值;value包含可迭代对象的下一个值。done为true状态时成为耗尽。
let arr = ['foo','zoo']
console.log(arr[Symbol.iterator]);
let iter = arr[Symbol.iterator]()
console.log(iter);
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
let arr1 = ['a','b']
let iter1 = arr1[Symbol.iterator]()
let iter2 = arr1[Symbol.iterator]()
console.log(iter1.next());
console.log(iter2.next());
console.log(iter1.next());
console.log(iter2.next());
let arr2 = ['foo','bar']
let iter3 = arr2[Symbol.iterator]()
console.log(iter3.next());
arr2.splice(1,0,'bzz')
console.log(iter3.next());
console.log(iter3.next());
console.log(iter3.next());
1.2.3 自定义迭代器
- 与Iterable接口类似,任何实现Iterator接口的对象都可以作为迭代器使用。
class Counter {
constructor(limit){
this.count = 1
this.limit = limit
}
next(){
if(this.count <= this.limit){
return { done: false, value: this.count++ }
}else{
return { done: true, value: undefined }
}
}
[Symbol.iterator](){
return this
}
}
let counter = new Counter(3)
for (let iterator of counter) {
console.log(iterator);
}
for (let i of counter) {
console.log(i);
console.log(111);
}
- 为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器,为此,可以把计数器变量放在闭包里,然后通过闭包返回迭代器。
class Counter {
constructor(limit){
this.limit = limit
}
[Symbol.iterator](){
let count = 1
let 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);
}
for (let i of counter) {
console.log(i);
}
1.2.4 提前终止迭代器
- 可选的return()方法用于指定在迭代器提前关闭时执行的逻辑,可能的出现提前关闭的情况:
- for of 循环通过break contiune return 或 throw提前退出
- 解构操作并未消费所有值
- return()方法必须返回一个有效的IteratorResult对象,简单情况下,可以只返回{ done: true, value: undefined }
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
return() {
console.log("Exiting early...");
return { done: true, value: undefined };
},
};
}
}
let counter = new Counter(3);
for (const i of counter) {
if (i > 2) {
break;
}
console.log(i);
}
let counter2 = new Counter(5);
try {
for (const i of counter2) {
if (i > 2) {
throw "err";
}
console.log(i);
}
} catch (error) {}
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();
for (const i of iter) {
console.log(i);
if (i > 3) {
break;
}
}
for (const i of iter) {
console.log(i);
}
1.3 生成器
- 生成器是ECMAScript6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
- 生成器的形式是一个函数,函数名称前面加一个星号( * )表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。
- (箭头函数不能用来定义生成器函数),标识生成器函数的星号不受两侧空格的影响。
function* generatorFn(){}
let generatorFn2 = function* (){}
const foo = {
*generatorFn(){}
}
class Foo {
*generatorFn(){}
}
class Animal {
static * generatorFn(){}
}
- 调用生成器函数会产生一个
生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。
- next()方法的返回值类似于迭代器,有done和value属性,函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done:true状态。
- value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定:
- 生成器函数只会在初次调用next()方法后开始执行。
function* generatorFn(){}
const fn = generatorFn()
console.log(fn);
console.log(fn.next());
let generatorFn2 = function* (){
return 'foo'
}
let fn2 = generatorFn2()
console.log(fn2.next());
let generatorFn3 = function *() {
console.log('foobar');
}
let fn3 = generatorFn3()
console.log(fn3);
fn3.next()
- 生成器对象实现了Iterable接口,它们默认的迭代器是自引用的。
function *generatorFn(){}
console.log(generatorFn);
console.log(generatorFn()[Symbol.iterator]);
console.log(generatorFn()[Symbol.iterator]());
const g = generatorFn()
console.log(g === g[Symbol.iterator]());
1.3.1 通过yield中断执行
- yield关键字可以让生成器停止和开始执行,生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。
- yield关键字像函数中间返回语句,它生成的值会出现在next()方法返回的对象里。通过yield关键字退出的生成器函数会处于done:false的状态;通过return关键字退出的生成器函数会处于done:true状态。
- 生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器。
- yield关键字只能在生成器函数内部使用,用在其他地方会抛出错误。
function *generatorFn(){
yield 'foo'
yield 'bar'
return 'baz'
}
let g = generatorFn()
console.log(g.next());
console.log(g.next());
console.log(g.next());
let g1 = generatorFn()
console.log(g1.next());
- 生成器对象作为可迭代对象
function *generatorFn(){
yield 'foo'
yield 'bar'
yield 'baz'
}
for (const x of generatorFn()) {
console.log(x);
}
- 使用yield实现输入和输出,除了可以作为函数的中间返回语句使用,yield关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。
function *generatorFn(initial){
console.log(initial);
console.log(yield);
console.log(yield);
}
let g = generatorFn('foo')
g.next()
g.next('bar')
g.next('baz')
function *generatorFn2(){
return yield 'foo'
}
let g2 = generatorFn2()
console.log(g2.next());
console.log(g2.next('bar'));
- 产生可迭代对象, 可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
function *generatorFn(){
yield* [1,2,3]
}
let g = generatorFn()
for (const x of g) {
console.log(x);
}
1.3.2 提前终止生成器
- 与迭代器类似,生成器也支持"可关闭",一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()
- return()和throw()方法都可以用于强制生成器进入关闭状态。
function *generator(){
for (const x of [1,2,3]) {
yield * x
}
}
let g = generator()
console.log(g);
console.log(g.return(4));
console.log(g);
console.log(g.next());
function *generatorFn(){
for (const x of [1,2,3]) {
yield x
}
}
let g2 = generatorFn()
console.log(g2);
try {
g2.throw('foo')
} catch (error) {
console.log(error);
}
console.log(g2.next());
function *generatorFn2(){
for (const x of [1,2,3]) {
try {
yield x
} catch (error) {
}
}
}
let g3 = generatorFn2()
console.log(g3.next());
g3.throw()
console.log(g3.next());
- 任何实现Iterable接口的对象都有一个Symbol.Iterator属性,这个属性引用默认的迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个Iterator接口对象。
- 生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了Iterable接口,因此可用在任何消费可迭代对象的地方。