1. 程式人生 > >Iterator 遍歷器的簡單使用

Iterator 遍歷器的簡單使用

Object介面 實現Iterator

我們可以使用 ES6 的展開運算子 … 和 for…of… 去遍歷帶有 Iterator 介面的資料結構,需要注意的是,Object 本身不具備 Iterator 介面,所以我們無法通過 … 把一個物件擴充套件到一個數組中,並且會報錯,我們可以通過程式碼手動將 Object 型別實現 Iterator 介面。

沒有實現Iterator 介面的Object


let obj = {
    a: 1,
    b: 2,
    c: 3
};

let arr = [...obj];

結果:

這裡寫圖片描述


實現Iterator 介面的Object

// 通過 Generator 函式給 Object 擴充套件 Iterator 介面
Object.prototype[Symbol.iterator] = function*() {
    for (var key in this) {
        yield this[key];
    }
};

// 測試 Iterator 介面
let obj = {
    a: 1,
    b: 2,
    c: 3
};

let arr = [...obj];

console.log(arr); // [1, 2, 3]

結果:
這裡寫圖片描述


模擬 Generator

Generator 函式是一個生成器,呼叫後會返回給我們一個 Iterator 遍歷器物件,在物件中有一個 next 方法,呼叫一次 next,幫我遍歷一次,返回值為一個物件,內部有 value 和 done 兩個屬性,value 屬性代表當前遍歷的值,done 屬性代表是否遍歷完成,如果遍歷完成後繼續呼叫 next,返回的物件中 value 屬性值為 undefined,done 屬性值為 true,這個遍歷器在進行資料遍歷時更像給我們提供了一個暫停功能,每次都需要手動呼叫 next 去進行下一次遍歷。

// 模擬遍歷器生成函式
function iterator(arr) {
    var i = 0;

    return {
        next: function() {
            var done = i >= arr.length;
            var value = !done ? arr[i++] : undefined;

            return {
                value: value,
                done: done
            };
        }
    };
}

測試一下模擬的遍歷器生成函式:

// 測試 iterator 函式
var arr = [1, 3, 5];

// 遍歷器
var result = iterator(arr);

result.next(); // {value: 1, done: false}
result.next(); // {value: 3, done: false}
result.next(); // {value: 5, done: false}
result.next(); // {value: undefined, done: true}

Generator 的基本使用

在普通的函式 function 關鍵字後加一個 * 就代表聲明瞭一個生成器函式,執行後返回一個遍歷器物件,每次呼叫遍歷器的 next 方法時,遇到 yield 關鍵字暫停執行,並將 yield 關鍵字後面的值會作為返回物件中 value 的值,如果函式有返回值,會把返回值作為呼叫 next 方法進行遍歷完成後返回的物件中 value 的值,果已經遍歷完成,再次 next 呼叫這個 value 的值會變成 undefined。

// 生成器函式
function* gen() {
    yield 1;
    yield 2;
    return 3;
}

// 遍歷器
let it = gen();

it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: true}
it.next(); // {value: undefined, done: true}

觀察兩個結果:

return
這裡寫圖片描述

無return

這裡寫圖片描述

在 Generator 函式中可以使用變數接收 yield 關鍵字執行後的返回值,只是接收的值並不是 yield 關鍵字後面表示式執行的結果,而是遍歷器在下一次呼叫 next 方法時傳入的引數。

在 Generator 函式中,如果在其他函式或方法呼叫的回撥內部(函式的執行上/下文發生變化)不能直接使用 yield 關鍵字。

/ 正確的寫法
function* gen(arr) {
    for(let i = 0; i < arr.length; i++) {
        yield arr[i];
    }
}

如果在一個 Generator 函式中呼叫了另一個 Generator 函式,在呼叫外層函式返回遍歷器的 next 方法時是不會遍歷內部函式返回的遍歷器的。

// 外層的生成器函式
function* genOut() {
    yield "a";
    yield genIn();
    yield "c";
}

// 內層的生成器函式
function* genIn() {
    yield "b";
}

// 遍歷器
let it = genOut();

it.next(); // {value: 'a', done: false}
it.next(); // 返回 genIn 的遍歷器物件
it.next(); // {value: 'c', done: false}
it.next(); // {value: undefined, done: true}

在 genOut 返回的遍歷器呼叫 next 遇到 yield* 表示式時幫我們去遍歷了 genIn 返回的遍歷器,其實 yield* 內部做了處理,等同於下面程式碼:

// 外層的生成器
function* genOut() {
    yield "a";
    for (let v of genIn()) {
        yield v;
    }
    yield "c";
}

// 內層的生成器
function* genIn() {
    yield "b";
}

// 遍歷器
let it = genOut();

it.next(); // {value: 'a', done: false}
it.next(); // {value: 'b', done: false}
it.next(); // {value: 'c', done: false}
it.next(); // {value: undefined, done: true}

Generators 與 Promise 結合

因為 Generator 函式在執行時遇到 yield 關鍵字會暫停執行,那麼 yield 後面可以是非同步操作的程式碼,比如 Promise,需要繼續執行,就手動呼叫返回遍歷器的 next 方法,因為中間有一個等待的過程,所以在執行非同步程式碼的時候避免了回撥函式的巢狀,在寫法上更像同步,更容易理解。

我們來設計一個 Generator 函式與 Promise 非同步操作結合的使用場景,假設我們需要使用 NodeJS 的 fs 模組讀取一個檔案 a.txt 的內容,而 a.txt 的內容是另一個需要讀取檔案 b.txt 的檔名,讀取 b.txt 最後列印讀取到的內容 “Hello world”。

// 引入依賴
let fs = require("fs");
let util = require("util");

// 將 readFile 方法轉換成 Promise
let read = util.promisify(fs.readFile);

// 生成器函式
function* gen() {
    let aData = yield read("1.txt", "utf8");
    let bData = yield read(aData, "utf8");
    return bData;
}

// 遍歷器
let it = gen();

it.next().value.then(data => {
    it.next(data).then(data => {
        console.log(data); // Hello world
    });
});

co 庫的使用

co 庫是基於 Generator 和 Promise 的,這個庫能幫我們實現自動呼叫 Generator 函式返回遍歷器的 next 方法,並執行 yield 後面 Promise 例項的 then 方法,所以每次 yield 後面的非同步操作返回的必須是一個 Promise 例項,程式碼看起來像同步,執行其實是非同步,不用自己手動進行下一次遍歷.