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 例項,程式碼看起來像同步,執行其實是非同步,不用自己手動進行下一次遍歷.