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

pingan8787發表於2019-02-16

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

系列目錄:

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

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

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

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)。

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
每日文章推薦 https://github.com/pingan8787…
JS小冊 js.pingan8787.com

歡迎關注微信公眾號【前端自習課】每天早晨,與您一起學習一篇優秀的前端技術博文 .

相關文章