【重溫基礎】13.迭代器和生成器

本文是 重溫基礎 系列文章的第十三篇。
今日感覺:每次自我年終總結,都會有各類情緒和收穫。html

系列目錄:前端

本章節複習的是JS中的迭代器和生成器,經常用來處理集合。node

前置知識:
JavaScrip已經提供多個迭代集合的方法,從簡單的for循環到map()filter()
迭代器和生成器將迭代的概念直接帶入核心語言,並提供一種機制來自定義for...of循環的行爲。git

本文會將知識點分爲兩大部分,簡單介紹和詳細介紹
簡單介紹,適合基礎入門會使用的目標;
詳細介紹,會更加深刻的作介紹,適合理解原理;github

1. 概述

當咱們使用循環語句迭代數據時,需初始化一個變量來記錄每一次迭代在數據集合中的位置:正則表達式

let a = ["aaa","bbb","ccc"];
for (let i = 0; i< a.length; i++){
    console.log(a[i]);
}
複製代碼

這邊的i就是咱們用來記錄迭代位置的變量,可是在ES6開始,JavaScrip引入了迭代器這個特性,而且新的數組方法新的集合類型(如Set集合Map集合)都依賴迭代器的實現,這個新特性對於高效的數據處理而言是不可或缺的,在語言的其餘特性中也都有迭代器的身影:新的for-of循環、展開運算符(...),甚至連異步編程均可以使用迭代器。編程

本文主要會介紹ES6中新增的迭代器(Iterator)和生成器(Generator)。json

2. 迭代器(簡單介紹)

迭代器是一種特殊對象,它具備一些專門爲迭代過程設計的專有接口,全部的迭代器對象都有一個next()方法,每次調用都會返回一個結果對象。
這個結果對象,有兩個屬性:數組

  • value: 表示下一個將要返回的值。
  • done: 一個布爾值,若沒有更多可返回的數據時,值爲true,不然false

若是最後一個值返回後,再調用next(),則返回的對象的done值爲true,而value值若是沒有值的話,返回的爲undefined微信

ES5實現一個迭代器:

function myIterator(list){
    var i = 0;
    return {
        next: function(){
            var done = i >= list.length;
            var value = !done ? list[i++] : undefined;
            return {
                done : done,
                value : value
            }
        }
    }
}

var iterator = myIterator([1,2,3]);
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 之後的調用都同樣
iterator.next();  // "{done: true, value: undefined}"
複製代碼

從上面代碼能夠看出,ES5的實現仍是比較麻煩,而ES6新增的生成器,可使得建立迭代器對象的過程更加簡單。

3. 生成器(簡單介紹)

生成器是一種返回迭代器的函數,經過function關鍵字後的星號(*)來表示,函數中會用到新的關鍵字yield。星號能夠緊挨着function關鍵字,也能夠在中間添加一個空格。

function *myIterator(){
    yield 1;
    yield 2;
    yield 3;
}
let iterator = myIterator();
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 之後的調用都同樣
iterator.next();  // "{done: true, value: undefined}"
複製代碼

生成器函數最有趣的部分是,每當執行完一條yield語句後函數就會自動中止執行,好比上面代碼,當yield 1;執行完後,便不會執行任何語句,而是等到再調用迭代器的next()方法纔會執行下一個語句,即yield 2;.
使用yield關鍵字能夠返回任何值和表達式,由於能夠經過生成器函數批量給迭代器添加元素:

function *myIterator(list){
    for(let  i = 0; i< list.length ; i ++){
        yield list[i];
    }
}

var iterator = myIterator([1,2,3]);
iterator.next();  // "{done: false, value: 1}"
iterator.next();  // "{done: false, value: 2}"
iterator.next();  // "{done: false, value: 3}"
iterator.next();  // "{done: true, value: undefined}"
// 之後的調用都同樣
iterator.next();  // "{done: true, value: undefined}"
複製代碼

生成器的適用返回很廣,能夠將它用於全部支持函數使用的地方。

4. 迭代器(詳細介紹)

4.1 Iterator迭代器概念

Iterator是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成迭代操做(即依次處理該數據結構的全部成員)。

Iterator三個做用

  • 爲各類數據結構,提供一個統一的、簡便的訪問接口;
  • 使得數據結構的成員可以按某種次序排列;
  • Iterator 接口主要供ES6新增的for...of消費;

4.2 Iterator迭代過程

  1. 建立一個指針對象,指向當前數據結構的起始位置。也就是說,迭代器對象本質上,就是一個指針對象。
  2. 第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員。
  3. 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
  4. 不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

每一次調用next方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含valuedone兩個屬性的對象。

  • value屬性是當前成員的值;
  • done屬性是一個布爾值,表示迭代是否結束;

模擬next方法返回值:

let f = function (arr){
    var nextIndex = 0;
    return {
        next:function(){
            return nextIndex < arr.length ?
            {value: arr[nextIndex++], done: false}:
            {value: undefined, done: true}
        }
    }
}

let a = f(['a', 'b']);
a.next(); // { value: "a", done: false }
a.next(); // { value: "b", done: false }
a.next(); // { value: undefined, done: true }
複製代碼

4.3 默認Iterator接口

若數據可迭代,即一種數據部署了Iterator接口。
ES6中默認的Iterator接口部署在數據結構的Symbol.iterator屬性,即若是一個數據結構具備Symbol.iterator屬性,就能夠認爲是可迭代
Symbol.iterator屬性自己是函數,是當前數據結構默認的迭代器生成函數。執行這個函數,就會返回一個迭代器。至於屬性名Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預約義好的、類型爲 Symbol 的特殊值,因此要放在方括號內(參見《Symbol》一章)。

原生具備Iterator接口的數據結構有

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象

4.4 Iterator使用場景

  • (1)解構賦值
    對數組和 Set 結構進行解構賦值時,會默認調用Symbol.iterator方法。
let a = new Set().add('a').add('b').add('c');
let [x, y] = a;       // x = 'a' y = 'b'
let [a1, ...a2] = a;  // a1 = 'a' a2 = ['b','c']
複製代碼
  • (2)擴展運算符
    擴展運算符(...)也會調用默認的 Iterator 接口。
let a = 'hello';
[...a];            // ['h','e','l','l','o']

let a = ['b', 'c'];
['a', ...a, 'd'];  // ['a', 'b', 'c', 'd']
複製代碼
  • (2)yield*
    yield*後面跟的是一個可迭代的結構,它會調用該結構的迭代器接口。
let a = function*(){
    yield 1;
    yield* [2,3,4];
    yield 5;
}

let b = a();
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // { value: 4, done: false }
b.next() // { value: 5, done: false }
b.next() // { value: undefined, done: true }
複製代碼
  • (4)其餘場合
    因爲數組的迭代會調用迭代器接口,因此任何接受數組做爲參數的場合,其實都調用了迭代器接口。下面是一些例子。

  • for...of

  • Array.from()

  • Map(), Set(), WeakMap(), WeakSet()(好比new Map([['a',1],['b',2]])

  • Promise.all()

  • Promise.race()

4.5 for...of循環

只要數據結構部署了Symbol.iterator屬性,即具備 iterator 接口,能夠用for...of循環迭代它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterato方法。
使用場景
for...of可使用在數組SetMap結構類數組對象Genetator對象字符串

  • 數組
    for...of循環能夠代替數組實例的forEach方法。
let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)}; // a b c

a.forEach((ele, index)=>{
    console.log(ele);    // a b c
    console.log(index);  // 0 1 2 
})
複製代碼

for...in對比,for...in只能獲取對象鍵名,不能直接獲取鍵值,而for...of容許直接獲取鍵值。

let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)};  // a b c
for (let k in a){console.log(k)};  // 0 1 2
複製代碼
  • Set和Map
    可使用數組做爲變量,如for (let [k,v] of b){...}
let a = new Set(['a', 'b', 'c']);
for (let k of a){console.log(k)}; // a b c

let b = new Map();
b.set('name','leo');
b.set('age', 18);
b.set('aaa','bbb');
for (let [k,v] of b){console.log(k + ":" + v)};
// name:leo
// age:18
// aaa:bbb
複製代碼
  • 類數組對象
// 字符串
let a = 'hello';
for (let k of a ){console.log(k)}; // h e l l o

// DOM NodeList對象
let b = document.querySelectorAll('p');
for (let k of b ){
    k.classList.add('test');
}

// arguments對象
function f(){
    for (let k of arguments){
        console.log(k);
    }
}
f('a','b'); // a b
複製代碼
  • 對象
    普通對象不能直接使用for...of會報錯,要部署Iterator才能使用。
let a = {a:'aa',b:'bb',c:'cc'};
for (let k in a){console.log(k)}; // a b c
for (let k of a){console>log(k)}; // TypeError
複製代碼

4.6 跳出for...of

使用break來實現。

for (let k of a){
    if(k>100)
        break;
    console.log(k);
}
複製代碼

5. 生成器(詳細介紹)

5.1 基本概念

Generator生成器函數是一種異步編程解決方案。
原理
執行Genenrator函數會返回一個遍歷器對象,依次遍歷Generator函數內部的每個狀態。
Generator函數是一個普通函數,有如下兩個特徵:

  • function關鍵字與函數名之間有個星號;
  • 函數體內使用yield表達式,定義不一樣狀態;

經過調用next方法,將指針移向下一個狀態,直到遇到下一個yield表達式(或return語句)爲止。簡單理解,Generator函數分段執行,yield表達式是暫停執行的標記,而next恢復執行。

function * f (){
    yield 'hi';
    yield 'leo';
    return 'ending';
}
let a = f();
a.next();  // {value: 'hi', done : false}
a.next();  // {value: 'leo', done : false}
a.next();  // {value: 'ending', done : true}
a.next();  // {value: undefined, done : false}
複製代碼

5.2 yield表達式

yield表達式是暫停標誌,遍歷器對象的next方法的運行邏輯以下:

  1. 遇到yield就暫停執行,將這個yield後的表達式的值,做爲返回對象的value屬性值。
  2. 下次調用next往下執行,直到遇到下一個yield
  3. 直到函數結束或者return爲止,並返回return語句後面表達式的值,做爲返回對象的value屬性值。
  4. 若是該函數沒有return語句,則返回對象的valueundefined

注意:

  • yield只能用在Generator函數裏使用,其餘地方使用會報錯。
// 錯誤1
(function(){
    yiled 1;  // SyntaxError: Unexpected number
})()

// 錯誤2 forEach參數是個普通函數
let a = [1, [[2, 3], 4], [5, 6]];
let f = function * (i){
    i.forEach(function(m){
        if(typeof m !== 'number'){
            yield * f (m);
        }else{
            yield m;
        }
    })
}
for (let k of f(a)){
    console.log(k)
}
複製代碼
  • yield表達式若是用於另外一個表達式之中,必須放在圓括號內。
function * a (){
    console.log('a' + yield);     // SyntaxErro
    console.log('a' + yield 123); // SyntaxErro
    console.log('a' + (yield));     // ok
    console.log('a' + (yield 123)); // ok
}
複製代碼
  • yield表達式用作函數參數或放在表達式右邊,能夠不加括號
function * a (){
    f(yield 'a', yield 'b');    // ok
    lei i = yield;              // ok
}
複製代碼

5.3 next方法

yield自己沒有返回值,或者是總返回undefinednext方法可帶一個參數,做爲上一個yield表達式的返回值。

function * f (){
    for (let k = 0; true; k++){
        let a = yield k;
        if(a){k = -1};
    }
}
let g =f();
g.next();    // {value: 0, done: false}
g.next();    // {value: 1, done: false}
g.next(true);    // {value: 0, done: false}
複製代碼

這一特色,可讓Generator函數開始執行以後,能夠從外部向內部注入不一樣值,從而調整函數行爲。

function * f(x){
    let y = 2 * (yield (x+1));
    let z = yield (y/3);
    return (x + y + z);
}
let a = f(5);
a.next();   // {value : 6 ,done : false}
a.next();   // {value : NaN ,done : false} 
a.next();   // {value : NaN ,done : true}
// NaN由於yeild返回的是對象 和數字計算會NaN

let b = f(5);
b.next();     // {value : 6 ,done : false}
b.next(12);   // {value : 8 ,done : false}
b.next(13);   // {value : 42 ,done : false}
// x 5 y 24 z 13
複製代碼

5.4 for...of循環

for...of循環會自動遍歷,不用調用next方法,須要注意的是,for...of遇到next返回值的done屬性爲true就會終止,return返回的不包括在for...of循環中。

function * f(){
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    return 5;
}
for (let k of f()){
    console.log(k);
}
// 1 2 3 4 沒有 5 
複製代碼

5.5 Generator.prototype.throw()

throw方法用來向函數外拋出錯誤,而且在Generator函數體內捕獲。

let f = function * (){
    try { yield }
    catch (e) { console.log('內部捕獲', e) }
}

let a = f();
a.next();

try{
    a.throw('a');
    a.throw('b');
}catch(e){
    console.log('外部捕獲',e);
}
// 內部捕獲 a
// 外部捕獲 b
複製代碼

5.6 Generator.prototype.return()

return方法用來返回給定的值,並結束遍歷Generator函數,若是return方法沒有參數,則返回值的value屬性爲undefined

function * f(){
    yield 1;
    yield 2;
    yield 3;
}
let g = f();
g.next();          // {value : 1, done : false}
g.return('leo');   // {value : 'leo', done " true}
g.next();          // {value : undefined, done : true}
複製代碼

5.7 next()/throw()/return()共同點

相同點就是都是用來恢復Generator函數的執行,而且使用不一樣語句替換yield表達式。

  • next()yield表達式替換成一個值。
let f = function * (x,y){
    let r = yield x + y;
    return r;
}
let g = f(1, 2); 
g.next();   // {value : 3, done : false}
g.next(1);  // {value : 1, done : true}
// 至關於把 let r = yield x + y;
// 替換成 let r = 1;
複製代碼
  • throw()yield表達式替換成一個throw語句。
g.throw(new Error('報錯'));  // Uncaught Error:報錯
// 至關於將 let r = yield x + y
// 替換成 let r = throw(new Error('報錯'));
複製代碼
  • next()yield表達式替換成一個return語句。
g.return(2); // {value: 2, done: true}
// 至關於將 let r = yield x + y
// 替換成 let r = return 2;
複製代碼

5.8 yield* 表達式

用於在一個Generator中執行另外一個Generator函數,若是沒有使用yield*會沒有效果。

function * a(){
    yield 1;
    yield 2;
}
function * b(){
    yield 3;
    yield * a();
    yield 4;
}
// 等同於
function * b(){
    yield 3;
    yield 1;
    yield 2;
    yield 4;
}
for(let k of b()){console.log(k)}
// 3
// 1
// 2
// 4
複製代碼

5.9 應用場景

  1. 控制流管理
    解決回調地獄:
// 使用前
f1(function(v1){
    f2(function(v2){
        f3(function(v3){
            // ... more and more
        })
    })
})

// 使用Promise 
Promise.resolve(f1)
    .then(f2)
    .then(f3)
    .then(function(v4){
        // ...
    },function (err){
        // ...
    }).done();

// 使用Generator
function * f (v1){
    try{
        let v2 = yield f1(v1);
        let v3 = yield f1(v2);
        let v4 = yield f1(v3);
        // ...
    }catch(err){
        // console.log(err)
    }
}
function g (task){
    let obj = task.next(task.value);
  // 若是Generator函數未結束,就繼續調用
  if(!obj.done){
      task.value = obj.value;
      g(task);
  }
}
g( f(initValue) );
複製代碼
  1. 異步編程的使用 在真實的異步任務封裝的狀況:
let fetch = require('node-fetch');
function * f(){
    let url = 'http://www.baidu.com';
    let res = yield fetch(url);
    console.log(res.bio);
}
// 執行該函數
let g = f();
let result = g.next();
// 因爲fetch返回的是Promise對象,因此用then
result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
})
複製代碼

參考資料

1.MDN 迭代器和生成器
2.ES6中的迭代器(Iterator)和生成器(Generator)


本部份內容到這結束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推薦 github.com/pingan8787/…
ES小冊 js.pingan8787.com

歡迎關注個人微信公衆號【前端自習課】

相關文章
相關標籤/搜索