红宝书-第七章-迭代器与生成器
迭代器
-
在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。
-
Array.prototype.forEach()
方法对数组的每个元素执行一次给定的函数,但没有办法标识迭代何时终止,且只适用于数组。 -
迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现了正式的Iterable 接口,而且可以通过迭代器Iterator 消费。
-
任何实现Iterable 接口的数据结构都可以被实现Iterator 接口的结构“消费”。**迭代器(iterator)**是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。
-
可迭代协议:
-
实现Iterable 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator 接口的对象的能力。
-
在ECMAScript 中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
-
很多内置类型都实现了Iterable接口:
- String、Array、map、set
- arguments对象
- NodeList等DOM集合类型
-
检查是否存在默认迭代器属性可以暴露这个工厂函数:
//这两种类型没有实现迭代器工厂函数 let num = 1; let obj = {}; console.log(num[Symbol.iterator]); console.log(num[Symbol.iterator]); //实现了迭代器工厂函数的类型 let str = 'abc'; // arr\map\set\...... console.log(str[Symbol.iterator]); // f values() { [native code] } console.log(str[Symbol.iterator]()); // StringIterator {}
-
实际上,不需要显示调用工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括:
- for-of循环
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all() 接收由期约组成的可迭代对象
- Promise.race() 接收由期约组成的可迭代对象
- yield* 操作符
在生成器中使用这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。
let arr = ['foo', 'bar', 'baz']; //for - of 循环 for(let el of arr) { console.log(el); } //数组解构 let [a,b,c] = arr; console.log(a,b,c); //扩展操作符 let arr2 = [...arr]; console.log(arr2); //Array.from() let arr3 = Array.from(arr); console.log(arr3); //Set构造函数 let set = new Set(arr); console.log(set); //Map构造函数 let pairs = arr.map((x,i) => [x,i]); console.log(pairs); // [['foo', 0], ['bar', 1], ['baz', 2]] let map = new Map(pairs); console.log(map); // Map(3) { 'foo'=>0, 'bar'=>1, 'baz'=>2 }
-
如果对象原型链上实现了Iterable接口,那这个对象也就实现了这个接口。
class FooArray extends Array {} let fooArr = new FooArray('foo', 'bar', 'baz'); for(let el of fooArr) { console.log(el); }
-
-
迭代器协议:
- 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。
- 迭代器API 使用next()方法在可迭代对象中遍历数据。
- 每次成功调用next(),都会返回一个IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用next(),则无法知道迭代器的当前位置。
- next()方法返回的迭代器对象IteratorResult 包含两个属性:done 和value。done 是一个布尔值,表示是否还可以再次调用next()取得下一个值;value 包含可迭代对象的下一个值(done false),或者undefined(done 为true)。done: true 状态称为“耗尽”。
- 只要迭代器到达done: true 状态,后续调用next()就一直返回同样的值。
- 迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。
- 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
-
自定义迭代器:
-
与Iterable 接口类似,任何实现Iterator 接口的对象都可以作为迭代器使用。
class Counter { // Counter 的实例应该迭代limit次 constructor(limit) { //this.count = 1; 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); }
-
为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,可以把计数器变量放到闭包里,然后通过闭包返回迭代器。
-
-
提前终止迭代器:
-
可选的return()方法用于指定在迭代器提前关闭时执行的逻辑。可能的情况包括:
- for-of 循环通过break、continue、return 或throw 提前退出。
- 解构操作并未消费所有值。
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 }; }, } // 定义return方法 return() { console.log('Exiting early'); return { done: true}; } }; } } // for-of循环,break会调用return方法 // 使用continue会直接提前终止循环且不会调用return方法 let counter1 = new Counter(5); for(let i of counter1) { if(i > 2) { break; } console.log(i); } //for-of循环,break let counter2 = new Counter(5); try { for(let i of counter2) { if(i > 2) { throw 'err'; } console.log(i); } } catch(e) {} //解构没有消费所有值调用return方法。 let counter3 = new Counter(5); let [a,b] = counter3; //Exiting early
-
因为return()方法是可选的,所以并非所有迭代器都是可关闭的,比如数组的迭代器不能关闭的。
-
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。
-
如果迭代器关闭了,那么下次重新使用迭代器会重头开始迭代。
-
生成器
-
生成器是ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
-
生成器基础:
-
生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。
//生成器函数声明 function* generatorFn() {} //生成器函数表达式 let generatorFn = function* () {} //作为对象字面量方法的生成器函数 let foo = { * generatorFn() {} } //作为类实例方法的生成器函数 class Foo { * generatorFn() {} } //作为类静态方法的生成器函数 class Bar { static * generatorFn() {} }
箭头函数不能用来定义生成器函数。
-
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行的状态。与迭代器相似,生成器对象也实现了Iterator接口,因此具有next() 方法。调用这个方法会让生成器开始或恢复执行。
-
next()方法的返回值类似于迭代器,有一个done 属性和一个value 属性。函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done: true 状态。
-
-
通过yield中断执行
-
yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。
function * generatorFn() { yield; } let generatorObject = generatorFn(); console.log(generatorObject.next()); //{done: false, value: undefined} console.log(generatorObject.next()); //{done: false, 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()不会影响其他生成器。
-
yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的return 关键字,yield 关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。
function * invalidGeneratorFnA() { function a() { yield; } }//无效
-
生成器对象作为可迭代对象
function * generatorFn() { yield 1; yield 2; yield 3; } for(const x of generatorFn() ) { console.log(x); } //传给生成器的函数可以控制迭代循环的次数 function * nTimes(n) { while(n--) { yield; } } for( let _ of nTimes(3)) { console.log('foo'); }
-
使用yield实现输入和输出
-
yield关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。(第一次调用next()传入的值不会被使用)
//yield关键字可同时用于输入和输出 function * generatorFn() { reeturn yield 'foo'; } let generatorObject = generatorFn(); //(输出)yield 生成的值会出现在next方法返回的对象中。 console.log(generatorObject.next()); // { done: false, value: 'foo'} //(输入)yield 接收next方法的第一个值 console.log(generatorObject.next('bar')); // { done: true, value: 'bar'}
因为函数必须对整个表达式求值才能确定要返回的值,所以在遇到yield 关键字时暂停执行并计算出要产生的值:“foo”。下一次调用next()传入了"bar",作为交给同一个yield 的值。然后这个值被确定为本次生成器函数要返回的值。
-
-
产生可迭代对象
-
可以使用星号增强yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
function * generatorFn() { yield * [1,2,3]; }
-
yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的。
-
yield*的值是关联迭代器返回done:true时的value属性。
-
-
使用yield*实现递归算法
-
yield*最有用的地方是实现递归操作,此时生成器可以产生自身。
function * nTimes(n) { if(n > 0) { yield * nTimes(n-1); yield n - 1; } } for( const x of nTimes(3)) { console.log(x); }
-
应用:图的dfs遍历
//顶点类 class Node { constructor(id) { this.id = id; this.neightbors = new Set(); } connect(node) { if(node !== this) { this.neighbors.add(node); node.neighbors.add(this); } } } class RandomGraph { constructor(size) { this.nodes = new Set(); //创建节点 for(let i = 0; i<size; i++) { this.nodes.add(new Node(i)); } //随机连接节点 const threshold = 1 / size; for( const x of this.nodes) { for( const y of this.nodes) { if( Math.random() < threshold) { x.connect(y); } } } } //调试 print() { for(const node of this.nodes) { const ids = [...node.neighbors].map((n) => n.id).join(','); console.log(`${node.id}: ${ids}`); } } //dfs遍历图是否是连通图 isConnected() { const visitedNodes = new Set(); function * traverse(nodes) { for( const node of nodes) { if( !visitedNodes.has(node)) { yield node; yield* traverse(node.neighbors); } } } //取得集合中的第一个节点 const firstNode = this.nodes[Symbol.iterator]().next().value; //使用递归生成器迭代每个节点 for( const node of traverse([firstNode])) { visitedNodes.add(node); } return visitedNodes.size === this.nodes.size; } }
-
-
-
生成器作为默认迭代器
-
因为生成器对象实现了Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
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); }
-
-
提前终止生成器
-
与迭代器类似,生成器也支持“可关闭”的概念。
-
一个实现Iterator 接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。
-
return()和throw()方法都可以用于强制生成器进入关闭状态。
-
与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。
-
throw():
-
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
-
假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的yield。
function * generatorFn() { for (const x of [1,2,3]) { try { yield x; } catch(e) {} } } const g = generatorFn(); console.log(g.next()); //{done: false, value: 1} g.throw("foo"); console.log(g.next()); // {done: false, value: 3}
throw()方法向生成器对象内部注入了一个错误:字符串"foo"。这个错误会被yield 关键字抛出。因为错误是在生成器的try/catch 块中抛出的,所以错误在生成器内部被捕获。由于yield 抛出了那个错误,生成器就不会再产出值2。此时,生成器函数继续执行,在下一次迭代再次遇到yield 关键字时产出了值3。
如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。(函数没运行就抛出错误,编译不通过)
-
-