JavaScript红宝书第七章:迭代器与生成器
理解迭代
什么是迭代?迭代就是有条件的循环,按顺序访问可迭代结构中的每一项。如下图:
javascript
for(let i=0;i<5;i++){
console.log(i)
}
实现了一个最简单的迭代计数循环。但如果说我们想用一个迭代方法去迭代多种不同数据类型的内容,ES5新增了ForEach方法往通用迭代迈进了但仍不理想。
javascript
let arr=[1,2,3,4]
arr.forEach(item=>console.log(item))//1,2,3,4
它解决了提取数组索引,和数组值问题,但是不能标识迭代数组的终止条件。所以ES6新增了一个迭代器模式 ,解决了通用迭代功能。
迭代器模式
迭代器模式是一个方案,它把有些结构称为可迭代对象 ,而这些对象都实现了Iterable接口,可以通过Iterator迭代器消费。而iterator是按需创建的一次性对象。iterrator迭代器会暴露关联的可迭代对象的迭代API,实现无需知道可迭代对象结构就能实现迭代。
可迭代对象
具有有限元素且无歧义遍历顺序的对象叫可迭代对象。
可迭代协议
实现迭代API(可迭代协议)需要具备两个能力:
- 支持迭代的自我识别能力
- 创建实现IteratorAPI的对象能力
所以说,每个可迭代对象都有一个默认迭代器属性,这个属性名必须是Symbol.Iterator且这个属性必须引用一个迭代器工厂函数,调用工厂函数必须返回一个新迭代器。
什么是工厂函数?
工厂函数类似于自动化机器,往这个函数里面投放它需要的参数就可以得到想要的产品。
实现Iterator接口的内置类型
- 字符串
- 数组
- 映射
- 集合
- arguments对象
- NodeList等Dom集合类型
什么是arguments?
可以移步我简述arguments的博客:原生JavaScript之函数特殊对象arguments
如何检查是否有迭代接口以及工厂函数
通过检查是否有[Symbol.Iterator]默认属性,来判断是否存在工厂函数。
javascript
let arr=[1,2,3,4]
let str='123'
let num=123
console.log(arr[Symbol.iterator]);//values() { [native code] }
console.log(str[Symbol.iterator]);// [Symbol.iterator]() { [native code] }
console.log(num[Symbol.iterator]);// undefined
而通过在这个默认属性加()即可调用迭代工厂函数。
javascript
let arr=[1,2,3,4]
let str='123'
console.log(arr[Symbol.iterator]());// ay Iterator {}
console.log(str[Symbol.iterator]());// ingIterator {}
实现写代码过程中,我们不需要通过调用工厂函数生成迭代器,可以直接调用可迭代对象通用的方法特性来实现迭代:
- for of
- 数组解构
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all
- Promise.race
- yield *(生成器)
实例:
javascript
let arr=[1,2,3,4]
// for of
for(item of arr){
console.log(item);//1 2 3 4
}
// 数组解构
let [a,b,c]=arr
console.log(a,b,c);//1 2 3
// 扩展操作符
let arr2=[...arr]
console.log(arr2);//[1,2,3,4]
// Array.from
let arr3=Array.from(arr)
console.log(arr3);//[1,2,3,4]
// Set 构造函数
let set = new Set(arr);
console.log(set); // Set(4) {'1', '2', '3',4}
// Map 构造函数
let pairs = arr.map((x, i) => [x, i]);
console.log(pairs); // [['1', 0], ['2', 1], ['3', 2],[4,3]]
let map = new Map(pairs);
console.log(map); // Map(3) { '1'=>0, '2'=>1, '3'=>2 ,4=>3}
迭代器协议
迭代器是一次性使用对象,它的API使用next方法进行迭代遍历,next一次,则返回一个迭代结果对象,包含迭代器下一个返回值,不调用next就不知道当前迭代器位置。
next方法
调用next方法返回对象的属性:done和value
done是用于判断是否还有下一个值的布尔(有则false,无则true),value是下一个值,没有则是undefined。
javascript
let arr = ['foo', 'bar'];
let iter1 = arr[Symbol.iterator]();
console.log(iter1.next()); // { done: false, value: 'foo' }
console.log(iter1.next()); // { done: false, value: 'bar' }
console.log(iter1.next()); // {value: undefined, done: true}
自定义迭代器
javascript
class Counter {
// Counter 的实例应该迭代 limit 次
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 i of counter) {
console.log(i);
}
// 1
// 2
// 3
可以自己实现一个自定义迭代,里面有迭代的返回条件以及对应的返回值。这里就不赘述了。
提前终止迭代器
不想遍历到终止,想要提前终止怎么办?
for-of可以通过break、continue、return、throw提前退出。也可以自己设置一些终止条件来进行判断是否终止。
如果我们不想终止迭代,但是想暂停某个自定义迭代函数块,在特定条件下暂停或者继续,就可以用到生成器来进行操作。
生成器
定义
ES6新增结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种新能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。
javascript
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}
// 作为类实例方法的生成器函数
class Foo {
* generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn() {}
}
注:箭头函数不能用来定义生成器函数。
生成器函数声明就是在普通函数名前加*即可。而调用这个生成器会出现一个生成器对象 ,该对象初始为暂停执行状态。同时它也实现了Iterator接口,也有next方法,调用next会让生成器开始或恢复执行,同样next有两个属性done和value,当生成器函数体为空时done为true,value为undefined,想要修改默认value,可通过修改返回值来不让空函数体默认为undefined。
javascript
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i;
}
return iterationCount;
}
上述代码中的yield关键字,用于生成器中断执行和继续执行。通过yield关键字退出的生成器,返回的done是false。
javascript
function* a(){
yield;
}
console.log(a().next())//done:false,value:undefined
注:yield关键字只能在生成器函数内部使用。
yield可以干嘛
生成器对象可以作为可迭代对象
javascript
function* generatorFn(){
yield 1;
yield 2;
yield 3;
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
使用 yield 实现输入和输出
javascript
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
也可以同时输入输出
javascript
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' }
产生可迭代对象
javascript
function* generatorFn() {
yield* [1, 2, 3];
}
let generatorObject = generatorFn();
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
使用 yield*实现递归算法
javascript
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2
提前终止生成器
return方法
javascript
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>}
throw方法
javascript
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try {
g.throw('foo');
} catch (e) {
console.log(e); // foo
}
console.log(g); // generatorFn {<closed>}