[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方法:調用該方法,可以在生成器中產生一個錯誤