[toc]
### 迭代器
#### 背景知识
1. 什么是迭代?
从一个数据集合中按照一定的顺序,不断取出数据的过程
2. 迭代和遍历的区别?
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
遍历强调的是要把整个数据依次全部取出
3. 迭代器
迭代器是对迭代过程的一个封装,在不同的语言中有不同的表现形式,通常为对象
4. 迭代模式
一种设计模式,用于统一迭代过程,并规范了迭代器规格:
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
#### JS中的迭代器
JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:
```js
{value: 值, done: 是否迭代完成}
```
则认为该对象是一个迭代器
含义:
- next方法:用于得到下一个数据
- 返回的对象
- value:下一个数据的值
- done:boolean,是否迭代完成
例如:我们可以通过迭代器来获取斐波那契数列中某一位的值。
```js
function createFeiboIterator() { //定义一个斐波那契数列生成器
let prev1 = 1, //当前位置的前一位的值
prev2 = 1, //当前位置的前两位的值
n = 1; //当前是第几位
return {
next() {
let value;
if (n <= 2) {
value = 1;
} else {
value = prev1 + prev2;
}
const result = {
value,
done: false,
};
prev2 = prev1;
prev1 = result.value;
n++;
return result;
},
};
}
const fbNum = createFieboIterator(); //生成一个斐波那契数列的可迭代对象
fbNum.next(); //输出:{value:1, done:false}
fbNum.next(); //输出:{value:1, done:false}
fbNum.next(); //输出:{value:2, done:false}
fbNum.next(); //输出:{value:3, done:false}
fbNum.next(); //输出:{value:5, done:false}
fbNum.next(); //输出:{value:8, done:false}
```
### 可迭代协议 与 for-of 循环
#### 可迭代协议
**概念回顾**
- 迭代器(iterator):一个具有next方法的对象,next方法返回下一个数据并且能指示是否迭代完成
- 迭代器创建函数(iterator creator):一个返回迭代器的函数
**可迭代协议**
ES6规定,如果一个对象具有知名符号属性```Symbol.iterator```,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)
例如:
```js
var obj = {
a: 1,
b: 2,
[Symbol.iterator]() {
const keys = Object.keys(this);
let i = 0;
return {
next: () => {
const propName = keys[i];
const propValue = this[propName];
const result = {
value: {
propName,
propValue
},
done: i >= keys.length
}
i++;
return result;
}
}
}
}
//按照上面的方法创建一个obj对象,我们为obj对象手动添加了[Symbol.iterator]属性,那么该对象就是一个可迭代对象
```
> 思考:如何知晓一个对象是否是可迭代的?
看一下该对象是否具有"Symbol.iterator"属性,并且其属性值是一个迭代器创建函数。
例如:在ES6中,数组本身就是一个可迭代对象。其prototype上本身具有Symbol.iterator属性。
```js
const arr = [1,2,3,4,5,6]; //创建一个数组
const arrIterator = arr[Symbol.iterator](); //使用数组原型中的Symbol.iterator属性创建一个该数组的迭代器
console.log(arrIterator.next()); //第一次执行会返回{value:1, done:false},再次执行会依次向后迭代。
```
在web Api中DOM对象等伪数组也是可迭代对象。
> 思考:如何遍历一个可迭代对象?
最基本的方法,可以使用while循环来遍历一个可迭代对象中的每一项的值。
```js
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const item = result.value; //取出数据
console.log(item);
//下一次迭代
result = iterator.next();
}
```
#### for-of 循环
基于上面的遍历模式,ES6为我们提供了一个新的方法:for-of 循环用于遍历可迭代对象,格式如下
```js
//迭代完成后循环结束
for(const item of iterable){
//iterable:可迭代对象
//item:每次迭代得到的数据
}
```
基于for-of循环,遍历上面的数组可以写成:
```js
for (const item of arr){
console.log(item)
}
//执行结果:依次输出1,2,3,4,5,6
```
#### 展开运算符与可迭代对象
**展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。**
对可迭代对象使用展开运算符,相当于对该对象使用了for-of,并将其返回的每个值追加到数组中。
### 生成器 (Generator)
1. 什么是生成器?
生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象
2. 如何创建生成器?
生成器的创建,必须使用生成器函数(Generator Function)
3. 如何书写一个生成器函数呢?
只需要在function后面或定义的函数名前面加上*号即可。
```js
//这是一个生成器函数,该函数一定返回一个生成器
function* test(){
}
const iterator = test(); //调用该函数,并没有执行函数体中的代码,而是创建了一个可迭代对象iterator
for(const item of iterator){}; //执行这条语句并没有报错,说明iterator是一个可迭代对象。
```
4. 生成器函数内部是如何执行的?
生成器函数内部书写的代码,是为了给生成器的每次迭代提供数据的。
生成器生成可迭代对象后,只有该可迭代对象调用next方法时才会执行生成器函数体内部的代码。
每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置
yield是一个关键字,该关键字只能在生成器函数内部使用,表示“产生”一个迭代数据。
代码示例:
```js
function* test(){
console.log('第1次运行next');
yield first; //位置A
console.log('第2次运行next');
yield second; //位置B
console.log('第3次运行next'); //位置C
}
const ite = test(); //调用test()生成器,创建一个可迭代对象ite,此时test函数体中的代码不会执行任何语句
console.log(ite.next());
//当第一执行ite的next方法时,开始执行test中的代码,从第一行开始执行到第一个yield的位置,即执行到位置A即停止执行。并将yield后面的值做为value的值返回。此时输出的结果为:第1次运行next {value: 'first', done: false}
console.log(ite.next());
//当再次执行ite的next方法时,继续执行test中的代码,从位置A开始执行到第二个yield的位置,即执行到位置B即停止执行。并将yield后面的值做为value的值返回。此时输出的结果为:第2次运行next {value: 'second', done: false}
console.log(ite.next());
//当第三次执行ite的next方法时,继续执行test中的代码,从位置B开始执行到代码结束,停止执行。因为代码已结束,没有yield关键字返回值。此时输出的结果为:第3次运行next {value: undefined, done: true}。表示迭代已完成
```
>由此总结,执行由生成器生成的可迭代对象的next方法时,会从第一句开始执行生成器函数体中的语句,当遇到yield关键字时停止向下执行语句,并将yield的值做为value值返回。再次执行next方法时,会从上一次yield位置的下一条语句开始执行,直到遇到下一个yield关键字停止。
根据生成器的规则,我们可以简化上节中斐波那契生成器的代码
```js
//创建一个斐波那契数列的生成器
function* createFeiboIterator() {
let prev1 = 1,
prev2 = 1, //当前位置的前1位和前2位
n = 1; //当前是第几位
while (true) {
if (n <= 2) {
yield 1;
} else {
const newValue = prev1 + prev2
yield newValue;
prev2 = prev1;
prev1 = newValue;
}
n++;
}
}
const fbNum = createFieboIterator(); //生成一个斐波那契数列的可迭代对象
fbNum.next(); //输出:{value:1, done:false}
fbNum.next(); //输出:{value:1, done:false}
fbNum.next(); //输出:{value:2, done:false}
fbNum.next(); //输出:{value:3, done:false}
fbNum.next(); //输出:{value:5, done:false}
fbNum.next(); //输出:{value:8, done:false}
```
>生成器的目的就是使迭代器的代码更加简洁。
5. 有哪些需要注意的细节?
1). 生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中
2). 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
3). 第一次调用next方法时,传参没有任何意义
4). 在生成器函数内部,可以调用其他生成器函数,但是要注意在yield后面加上*号
例如:
```js
function* test1(){
yield a;
yield b;
}
function* test(){
yield* test1();
yield 1
yield 2
}
const t = test();
t.next(); //{value:a, done:false}
t.next(); //{value:b, done:false}
t.next(); //{value:1, done:false}
```
6. 生成器的其他API
- return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
- throw方法:调用该方法,可以在生成器中产生一个错误