深入解析ES6中的迭代器(Iterator)和生成器(Generator)

用循環語句迭代數據時,必須要初始化一個變量來記錄每一次迭代在數據集合中的位置,而在許多編程語言中,已經開始通過程序化的方式用迭代器對象返回迭代過程中集合的每一個元素

迭代器的使用可以極大地簡化數據操作,於是ES6也向JS中添加了這個迭代器特性。新的數組方法和新的集合類型(如Set集合與Map集合)都依賴迭代器的實現,這個新特性對於高效的數據處理而言是不可或缺的,在語言的其他特性中也都有迭代器的身影:新的for-of循環、展開運算符(...),甚至連異步編程都可以使用迭代器

本文將詳細介紹ES6中的迭代器(Iterator)和生成器(Generator)

引入

下面是一段標準的for循環代碼,通過變量i來跟蹤colors數組的索引,循環每次執行時,如果i小於數組長度len則加1,並執行下一次循環

var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}

雖然循環語句語法簡單,但如果將多個循環嵌套則需要追蹤多個變量,代碼複雜度會大大增加,一不小心就錯誤使用了其他for循環的跟蹤變量,從而導致程序出錯。迭代器的出現旨在消除這種複雜性並減少循環中的錯誤

迭代器

迭代器是一種特殊對象,它具有一些專門為迭代過程設計的專有接口,所有的迭代器對象都有一個next()方法,每次調用都返回一個結果對象。結果對象有兩個屬性:一個是value,表示下一個將要返回的值;另一個是done,它是一個布爾類型的值,當沒有更多可返回數據時返回true。迭代器還會保存一個內部指針,用來指向當前集合中值的位置,每調用一次next()方法,都會返回下一個可用的值

如果在最後一個值返回後再調用next()方法,那麼返回的對象中屬性done的值為true,屬性value則包含迭代器最終返回的值,這個返回值不是數據集的一部分,它與函數的返回值類似,是函數調用過程中最後一次給調用者傳遞信息的方法,如果沒有相關數據則返回undefined

下面用ES5的語法創建一個迭代器

function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 之後的所有調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

在上面這段代碼中,createIterator()方法返回的對象有一個next()方法,每次調用時,items數組的下一個值會作為value返回。當i為3時,done變為true;此時三元表達式會將value的值設置為undefined。最後兩次調用的結果與ES6迭代器的最終返回機制類似,當數據集被用盡後會返回最終的內容

上面這個示例很複雜,而在ES6中,迭代器的編寫規則也同樣複雜,但ES6同時還引入了一個生成器對象,它可以讓創建迭代器對象的過程變得更簡單

生成器

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

// 生成器
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
// 生成器能像正規函數那樣被調用,但會返回一個迭代器

let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

在這個示例中,createlterator()前的星號表明它是一個生成器;yield關鍵字也是ES6的新特性,可以通過它來指定調用迭代器的next()方法時的返回值及返回順序。生成迭代器後,連續3次調用它的next()方法返回3個不同的值,分別是1、2和3。生成器的調用過程與其他函數一樣,最終返回的是創建好的迭代器

生成器函數最有趣的部分是,每當執行完一條yield語句後函數就會自動停止執行。舉個例子,在上面這段代碼中,執行完語句yield 1之後,函數便不再執行其他任何語句,直到再次調用迭代器的next()方法才會繼續執行yield 2語句。生成器函數的這種中止函數執行的能力有很多有趣的應用

使用yield關鍵字可以返回任何值或表達式,所以可以通過生成器函數批量地給迭代器添加元素。例如,可以在循環中使用yield關鍵字

function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

// 之後的所有調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

在此示例中,給生成器函數createlterator()傳入一個items數組,而在函數內部,for循環不斷從數組中生成新的元素放入迭代器中,每遇到一個yield語句循環都會停止;每次調用迭代器的next()方法,循環會繼續運行並執行下一條yield語句

生成器函數是ES6中的一個重要特性,可以將其用於所有支持函數使用的地方

【使用限制】

yield關鍵字只可在生成器內部使用,在其他地方使用會導致程序拋出錯誤

function *createIterator(items) {
items.forEach(function(item) {
// 語法錯誤
yield item + 1;
});
}

從字面上看,yield關鍵字確實在createlterator()函數內部,但是它與return關鍵字一樣,二者都不能穿透函數邊界。嵌套函數中的return語句不能用作外部函數的返回語句,而此處嵌套函數中的yield語句會導致程序拋出語法錯誤

【生成器函數表達式】

也可以通過函數表達式來創建生成器,只需在function關鍵字和小括號中間添加一個星號(*)即可

let createIterator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 之後的所有調用
console.log(iterator.next()); // "{ value: undefined, done: true }"

在這段代碼中,createlterator()是一個生成器函數表達式,而不是一個函數聲明。由於函數表達式是匿名的,因此星號直接放在function關鍵字和小括號之間。此外,這個示例基本與前例相同,使用的也是for循環

[注意]不能用箭頭函數來創建生成器

【生成器對象的方法】

由於生成器本身就是函數,因而可以將它們添加到對象中。例如,在ES5風格的對象字面量中,可以通過函數表達式來創建生成器

var o = {
createIterator: function *(items) {

for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);

也可以用ES6的函數方法的簡寫方式來創建生成器,只需在函數名前添加一個星號(*)

var o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);

這些示例使用了不同於之前的語法,但它們的功能實際上是等價的。在簡寫版本中,由於不使用function關鍵字來定義createlterator()方法,因此儘管可以在星號和方法名之間留白,但還是將星號緊貼在方法名之前

【狀態機】

生成器的一個常用功能是生成狀態機

let state = function*(){
while(1){
yield 'A';
yield 'B';
yield 'C';
}
}

let status = state();

console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'

可迭代對象

可迭代對象具有Symbol.iterator屬性,是一種與迭代器密切相關的對象。Symbol.iterator通過指定的函數可以返回一個作用於附屬對象的迭代器。在ES6中,所有的集合對象(數組、Set集合及Map集合)和字符串都是可迭代對象,這些對象中都有默認的迭代器。ES6中新加入的特性for-of循環需要用到可迭代對象的這些功能

[注意]由於生成器默認會為Symbol.iterator屬性賦值,因此所有通過生成器創建的迭代器都是可迭代對象

一開始,我們曾提到過循環內部索引跟蹤的相關問題,要解決這個問題,需要兩個工具:一個是迭代器,另一個是for-of循環。如此一來,便不需要再跟蹤整個集合的索引,只需關注集合中要處理的內容

for-of循環每執行一次都會調用可迭代對象的next()方法,並將迭代器返回的結果對象的value屬性存儲在一個變量中,循環將持續執行這一過程直到返回對象的done屬性的值為true。這裡有個示例

let values = [1, 2, 3];
for (let num of values) {
//1
//2
//3
console.log(num);
}

這段for-of循環的代碼通過調用values數組的Symbol.iterator方法來獲取迭代器,這一過程是在JS引擎背後完成的。隨後迭代器的next()方法被多次調用,從其返回對象的value屬性讀取值並存儲在變量num中,依次為1、2和3,當結果對象的done屬性值為true時循環退出,所以num不會被賦值為undefined

如果只需迭代數組或集合中的值,用for-of循環代替for循環是個不錯的選擇。相比傳統的for循環,for-of循環的控制條件更簡單,不需要追蹤複雜的條件,所以更少出錯

[注意]如果將for-of語句用於不可迭代對象、null或undefined將會導致程序拋出錯誤

【訪問默認迭代器】

可以通過Symbol.iterator來訪問對象默認的迭代器

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

在這段代碼中,通過Symbol.iterator獲取了數組values的默認迭代器,並用它遍歷數組中的元素。在JS引擎中執行for-of循環語句時也會有類似的處理過程

由於具有Symbol.iterator屬性的對象都有默認的迭代器,因此可以用它來檢測對象是否為可迭代對象

function isIterable(object) {
return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); // true
console.log(isIterable("Hello")); // true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set())); // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

這裡的islterable()函數可以檢查指定對象中是否存在默認的函數類型迭代器,而for-of循環在執行前也會做相似的檢查

除了使用內建的可迭代對象類型的Symbol.iterator,也可以使用Symbol.iterator來創建屬於自己的迭代器

【創建可迭代對象】

默認情況下,開發者定義的對象都是不可迭代對象,但如果給Symbol.iterator屬性添加一個生成器,則可以將其變為可迭代對象

let collection = {
items: [],

*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
//1
//2
//3
console.log(x);
}

在這個示例中,先創建一個生成器(注意,星號仍然在屬性名前)並將其賦值給對象的Symbol.iterator屬性來創建默認的迭代器;而在生成器中,通過for-of循環迭代this.items並用yield返回每一個值。collection對象默認迭代器的返回值由迭代器this.items自動生成,而非手動遍歷來定義返回值

【展開運算符和非數組可迭代對象】

通過展開運算符(...)可以把Set集合轉換成一個數組

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]

這段代碼中的展開運算符把Set集合的所有值填充到了一個數組字面量裡,它可以操作所有可迭代對象,並根據默認迭代器來選取要引用的值,從迭代器讀取所有值。然後按照返回順序將它們依次插入到數組中。Set集合是一個可迭代對象,展開運算符也可以用於其他可迭代對象

let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "huochai"], ["age", 25]]

展開運算符把Map集合轉換成包含多個數組的數組,Map集合的默認迭代器返回的是多組鍵值對,所以結果數組與執行new Map()時傳入的數組看起來一樣

在數組字面量中可以多次使用展開運算符,將可迭代對象中的多個元素依次插入新數組中,替換原先展開運算符所在的位置

let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]

創建一個變量allNumbers,用展開運算符將smallNumbers和bigNumbers裡的值依次添加到allNumbers中。首先存入0,然後存入small中的值,最後存入bigNumbers中的值。當然,原始數組中的值只是被複制到allNumbers中,它們本身並未改變

由於展開運算符可以作用於任意可迭代對象,因此如果想將可迭代對象轉換為數組,這是最簡單的方法。既可以將字符串中的每一個字符(不是編碼單元)存入新數組中,也可以將瀏覽器中NodeList對象中的每一個節點存入新的數組中

內建迭代器

迭代器是ES6的一個重要組成部分,在ES6中,已經默認為許多內建類型提供了內建迭代器,只有當這些內建迭代器無法實現目標時才需要自己創建。通常來說當定義自己的對象和類時才會遇到這種情況,否則,完全可以依靠內建的迭代器完成工作,而最常使用的可能是集合的那些迭代器

【集合對象迭代器】

在ES6中有3種類型的集合對象:數組、Map集合與Set集合

為了更好地訪問對象中的內容,這3種對象都內建了以下三種迭代器

  • entries() 返回一個迭代器,其值為多個鍵值對
  • values() 返回一個迭代器,其值為集合的值
  • keys() 返回一個迭代器,其值為集合中的所有鍵名

調用以上3個方法都可以訪問集合的迭代器

entries()迭代器

每次調用next()方法時,entries()迭代器都會返回一個數組,數組中的兩個元素分別表示集合中每個元素的鍵與值。如果被遍歷的對象是數組,則第一個元素是數字類型的索引;如果是Set集合,則第一個元素與第二個元素都是值(Set集合中的值被同時作為鍵與值使用);如果是Map集合,則第一個元素為鍵名

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let entry of colors.entries()) {
console.log(entry);
}
for (let entry of tracking.entries()) {
console.log(entry);
}
for (let entry of data.entries()) {
console.log(entry);
}

調用console.log()方法後輸出以下內容

[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ES6"]
["format", "ebook"]

在這段代碼中,調用每個集合的entries()方法獲取一個迭代器,並使用for-of循環來遍歷元素,且通過console將每一個對象的鍵值對輸出出來

values()迭代器

調用values()迭代器時會返回集合中所存的所有值

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let value of colors.values()) {
console.log(value);
}
for (let value of tracking.values()) {
console.log(value);
}
for (let value of data.values()) {
console.log(value);
}

調用console.log()方法後輸出以下內容

"red"
"green"
"blue"
1234
5678
9012
"Understanding ES6"
"ebook"

如上所示,調用values()迭代器後,返回的是每個集合中包含的真正數據,而不包含數據在集合中的位置信息

keys()迭代器

keys()迭代器會返回集合中存在的每一個鍵。如果遍歷的是數組,則會返回數字類型的鍵,數組本身的其他屬性不會被返回;如果是Set集合,由於鍵與值是相同的,因此keys()和values()返回的也是相同的迭代器;如果是Map集合,則keys()迭代器會返回每個獨立的鍵

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for (let key of colors.keys()) {
console.log(key);
}
for (let key of tracking.keys()) {
console.log(key);
}
for (let key of data.keys()) {
console.log(key);
}

調用console.log()方法後輸出以下內容

0
1
2
1234
5678
9012
"title"
"format"

keys()迭代器會獲取colors、tracking和data這3個集合中的每一個鍵,而且分別在3個for-of循環內部將這些鍵名打印出來。對於數組對象來說,無論是否為數組添加命名屬性,打印出來的都是數字類型的索引;而for-in循環迭代的是數組屬性而不是數字類型的索引

不同集合類型的默認迭代器

每個集合類型都有一個默認的迭代器,在for-of循環中,如果沒有顯式指定則使用默認的迭代器。數組和Set集合的默認迭代器是values()方法,Map集合的默認迭代器是entries()方法。有了這些默認的迭代器,可以更輕鬆地在for-of循環中使用集合對象

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "print");
// 與使用 colors.values() 相同
for (let value of colors) {
console.log(value);
}
// 與使用 tracking.values() 相同
for (let num of tracking) {
console.log(num);
}
// 與使用 data.entries() 相同
for (let entry of data) {
console.log(entry);
}

上述代碼未指定迭代器,所以將使用默認的迭代器。數組、Set集合及Map集合的默認迭代器也會反應出這些對象的初始化過程,所以這段代碼會輸出以下內容

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ES6"]
["format", "print"]

默認情況下,如果是數組和Set集合,會逐一返回集合中所有的值。如果是Map集合,則按照Map構造函數參數的格式返回相同的數組內容。而WeakSet集合與WeakMap集合就沒有內建的迭代器,由於要管理弱引用,因而無法確切地知道集合中存在的值,也就無法迭代這些集合了

【字符串迭代器】

自ES5發佈以後,JS字符串慢慢變得更像數組了,例如,ES5正式規定可以通過方括號訪問字符串中的字符(也就是說,text[0]可以獲取字符串text的第一個字符,並以此類推)。由於方括號操作的是編碼單元而非字符,因此無法正確訪問雙字節字符

var message = "A  B" ;
for (let i=0; i < message.length; i++) {
console.log(message[i]);
}

在這段代碼中,訪問message的length屬性獲取索引值,並通過方括號訪問來迭代並打印一個單字符字符串,但是輸出的結果卻與預期不符

A

B

由於雙字節字符被視作兩個獨立的編碼單元,從而最終在A與B之間打印出4個空行

所幸,ES6的目標是全面支持Unicode,並且我們可以通過改變字符串的默認迭代器來解決這個問題,使其操作字符而不是編碼單元。現在,修改前一個示例中字符串的默認迭代器,讓for-of循環輸出正確的內容

var message = "A  B" ;
for (let c of message) {
console.log(c);
}

這段代碼輸出以下內容

A

B

這個結果更符合預期,通過循環語句可以直接操作字符併成功打印出Unicode字符

【NodeList迭代器】

DOM標準中有一個NodeList類型,document對象中的所有元素都用這個類型來表示。對於編寫Web瀏覽器環境中的JS開發者來說,需要花點兒功夫去理解NodeList對象和數組之間的差異。二者都使用length屬性來表示集合中元素的數量,都可以通過方括號來訪問集合中的獨立元素。而在內部實現中,二者的表現非常不一致,因而會造成很多困擾

自從ES6添加了默認迭代器後,DOM定義中的NodeList類型(定義在HTML標準而不是ES6標準中)也擁有了默認迭代器,其行為與數組的默認迭代器完全一致。所以可以將NodeList應用於for-of循環及其他支持對象默認迭代器的地方

var divs = document.getElementsByTagName("div");
for (let div of divs) {

console.log(div.id);
}

在這段代碼中,通過調用getElementsByTagName()方法獲取到document對象中所有div元素的列表,在for-of循環中遍歷列表中的每一個元素並輸出元素ID,實際上是按照處理數組的方式來處理NodeList的

高級迭代器

迭代器的基礎功能可以輔助完成很多任務,通過生成器創建迭代器的過程也很便捷,除了這些簡單的集合遍歷任務之外,迭代器也可以被用於完成一些複雜的任務

【給迭代器傳遞參數】

迭代器既可以用迭代器的next()方法返回值,也可以在生成器內部使用yield關鍵字來生成值。如果給迭代器的next()方法傳遞參數,則這個參數的值就會替代生成器內部上條yield語句的返回值。而如果要實現更多像異步編程這樣的高級功能,那麼這種給迭代器傳值的能力就變得至關重要

function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = createIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

第一次調用next()方法時無論傳入什麼參數都會被丟棄。由於傳給next()方法的參數會替代上一次yield的返回值,而在第一次調用next()方法前不會執行任何yield語句,因此在第一次調用next()方法時傳遞參數是毫無意義的

第二次調用next()方法傳入數值4作為參數,它最後被賦值給生成器函數內部的變量first。在一個含參yield語句中,表達式右側等價於第一次調用next()方法後的下一個返回值,表達式左側等價於第二次調用next()方法後,在函數繼續執行前得到的返回值。第二次調用next()方法傳入的值為4,它會被賦值給變量first,函數則繼續執行。第二條yield語句在第一次yield的結果上加了2,最終的返回值為6

第三次調用next()方法時,傳入數值5,這個值被賦值給second,最後用於第三條yield語句並最終返回數值8

【在迭代器中拋出錯誤】

除了給迭代器傳遞數據外,還可以給它傳遞錯誤條件。通過throw()方法,當迭代器恢復執行時可令其拋出一個錯誤。這種主動拋出錯誤的能力對於異步編程而言至關重要,也能提供模擬結束函數執行的兩種方法(返回值或拋出錯誤),從而增強生成器內部的編程彈性。將錯誤對象傳給throw()方法後,在迭代器繼續執行時其會被拋出

function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 2 ,然後拋出錯誤
yield second + 3; // 永不會被執行
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // 從生成器中拋出了錯誤

在這個示例中,前兩個表達式正常求值,而調用throw()方法後,在繼續執行let second求值前,錯誤就會被拋出並阻止了代碼繼續執行。這個過程與直接拋出錯誤很相似,二者唯一的區別是拋出的時機不同

可以在生成器內部通過try-catch代碼塊來捕獲這些錯誤

function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // yield 4 + 2 ,然後拋出錯誤
} catch (ex) {
second = 6; // 當出錯時,給變量另外賦值
}
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

在此示例中,try-catch代碼塊包裹著第二條yield語句。儘管這條語句本身沒有錯誤,但在給變量second賦值前還是會主動拋出錯誤,catch代碼塊捕獲錯誤後將second變量賦值為6,下一條yield語句繼續執行後返回9

這裡有一個有趣的現象調用throw()方法後也會像調用next()方法一樣返回一個結果對象。由於在生成器內部捕獲了這個錯誤,因而會繼續執行下一條yield語句,最終返回數值9

如此一來,next()和throw()就像是迭代器的兩條指令,調用next()方法命令迭代器繼續執行(可能提供一個值),調用throw()方法也會命令迭代器繼續執行,但同時也拋出一個錯誤,在此之後的執行過程取決於生成器內部的代碼

在迭代器內部,如果使用了yield語句,則可以通過next()方法和throw()方法控制執行過程,當然,也可以使用return語句返回一些與普通函數返回語句不太一樣的內容

【生成器返回語句】

由於生成器也是函數,因此可以通過return語句提前退出函數執行,對於最後一次next()方法調用,可以主動為其指定一個返回值。正如在其他函數中那樣,可以通過return語句指定一個返回值。而在生成器中,return表示所有操作已經完成,屬性done被設置為true;如果同時提供了相應的值,則屬性value會被設置為這個值

function *createIterator() {
yield 1;
return;

yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

這段代碼中的生成器包含多條yield語句和一條return語句,其中return語句緊隨第一條yield語句,其後的yield語句將不會被執行

在return語句中也可以指定一個返回值,該值將被賦值給返回對象的value屬性

function *createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

在此示例中,第二次調用next()方法時返回對象的value屬性值為42,done屬性首次設為true;第三次調用next()方法依然返回一個對象,只是value屬性的值會變為undefined。因此,通過return語句指定的返回值,只會在返回對象中出現一次,在後續調用返回的對象中,value屬性會被重置為undefined

[注意]展開運算符與for-of循環語句會直接忽略通過return語句指定的任何返回值,只要done一變為true就立即停止讀取其他的值。不管怎樣,迭代器的返回值依然是一個非常有用的特性

【委託生成器】

在某些情況下,我們需要將兩個迭代器合二為一,這時可以創建一個生成器,再給yield語句添加一個星號,就可以將生成數據的過程委託給其他生成器。當定義這些生成器時,只需將星號放置在關鍵字yield和生成器的函數名之間即可

function *createNumberIterator() {
yield 1;
yield 2;
}
function *createColorIterator() {
yield "red";
yield "green";
}
function *createCombinedIterator() {
yield *createNumberIterator();
yield *createColorIterator();
yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

這裡的生成器createCombinedIterator()先後委託了另外兩個生成器createNumberlterator()和createColorlterator()。僅根據迭代器的返回值來看,它就像是一個完整的迭代器,可以生成所有的值。每一次調用next()方法就會委託相應的迭代器生成相應的值,直到最後由createNumberlterator()和cpeateColorlterator()創建的迭代器無法返回更多的值,此時執行最後一條yield語句並返回true

有了生成器委託這個新功能,可以進一步利用生成器的返回值來處理複雜任務

function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

在生成器createCombinedlterator()中,執行過程先被委託給了生成器createNumberlterator(),返回值會被賦值給變量result,執行到return 3時會返回數值3。這個值隨後被傳入createRepeatinglterator()作為它的參數,因而生成字符串"repeat"的yield語句會被執行三次

無論通過何種方式調用迭代器next()方法,數值3都不會被返回,它只存在於生成器createCombinedlterator()的內部。但如果想輸出這個值,則可以額外添加一條yield語句

function *createNumberIterator() {
yield 1;
yield 2;
return 3;

}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield result;
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

此處新添加的yield語句顯式地輸出了生成器createNumberlterator()的返回值。

[注意]yield也可直接應用於字符串,例如yield "hello",此時將使用字符串的默認迭代器

異步任務執行

生成器令人興奮的特性多與異步編程有關,JS中的異步編程有利有弊:簡單任務的異步化非常容易;而複雜任務的異步化會帶來很多管理代碼的挑戰。由於生成器支持在函數中暫停代碼執行,因而可以深入挖掘異步處理的更多用法

執行異步操作的傳統方式一般是調用一個函數並執行相應回調函數

let fs = require("fs");
fs.readFile("config.json", function(err, contents) {
if (err) {
throw err;
}
doSomethingWith(contents);
console.log("Done");
});

調用fs.readFile()方法時要求傳入要讀取的文件名和一個回調函數,操作結束後會調用該回調函數並檢查是否存在錯誤,如果沒有就可以處理返回的內容。如果要執行的任務很少,那麼這樣的方式可以很好地完成任務;如若需要嵌套回調或序列化一系列的異步操作,事情會變得非常複雜。此時,生成器和yield語句就派上用場了

【簡單任務執行器】

由於執行yield語句會暫停當前函數的執行過程並等待下一次調用next()方法,因此可以創建一個函數,在函數中調用生成器生成相應的迭代器,從而在不用回調函數的基礎上實現異步調用next()方法

function run(taskDef) {
// 創建迭代器,讓它在別處可用
let task = taskDef();
// 啟動任務
let result = task.next();
// 遞歸使用函數來保持對 next() 的調用

function step() {
// 如果還有更多要做的
if (!result.done) {
result = task.next();
step();
}
}
// 開始處理過程
step();
}

函數run()接受一個生成器函數作為參數,這個函數定義了後續要執行的任務,生成一個迭代器並將它儲存在變量task中。首次調用迭代器的next()方法時,返回的結果被儲存起來稍後繼續使用。step()函數會檢查result.done的值,如果為false則執行迭代器的next()方法,並再次執行step()操作。每次調用next()方法時,返回的最新信息總會覆寫變量result。在代碼的最後,初始化執行step()函數並開始整個的迭代過程,每次通過檢查result.done來確定是否有更多任務需要執行

藉助這個run()函數,可以像這樣執行一個包含多條yield語句的生成器

run(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});

這個示例最終會向控制檯輸出多次調用next()方法的結果,分別為數值1、2和3。當然,簡單輸出迭代次數不足以展示迭代器高級功能的實用之處,下一步將在迭代器與調用者之間互相傳值

【向任務執行器傳遞數據】

給任務執行器傳遞數據的最簡單辦法是,將值通過迭代器的next()方法傳入作為yield的生成值供下次調用。在這段代碼中,只需將result.value傳入next()方法即可

function run(taskDef) {
// 創建迭代器,讓它在別處可用
let task = taskDef();
// 啟動任務
let result = task.next();
// 遞歸使用函數來保持對 next() 的調用
function step() {
// 如果還有更多要做的
if (!result.done) {
result = task.next(result.value);
step();
}
}
// 開始處理過程
step();
}

現在result.value作為next()方法的參數被傳入,這樣就可以在yield調用之間傳遞數據了

run(function*() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});

此示例會向控制檯輸出兩個數值1和4。其中,數值1取自yield 1語句中回傳給變量value的值;而4取自給變量value加3後回傳給value的值。現在數據已經能夠在yield調用間互相傳遞了,只需一個小小改變便能支持異步調用

【異步任務執行器】

之前的示例只是在多個yield調用間來回傳遞靜態數據,而等待一個異步過程有些不同。任務執行器需要知曉回調函數是什麼以及如何使用它。由於yield表達式會將值返回給任務執行器,所有的函數調用都會返回一個值,因而在某種程度上這也是一個異步操作,任務執行器會一直等待直到操作完成

下面定義一個異步操作

function fetchData() {
return function(callback) {
callback(null, "Hi!");
};
}

在這個版本的fetchData()函數中,讓回調函數延遲了50ms再被調用,所以這種模式在同步和異步狀態下都運行良好。只需保證每個要通過yield關鍵字調用的函數都按照與之相同的模式編寫

理解了函數中異步過程的運作方式,可以將任務執行器稍作修改。當result.value是一個函數時,任務執行器會先執行這個函數再將結果傳入next()方法

function run(taskDef) {
// 創建迭代器,讓它在別處可用

let task = taskDef();
// 啟動任務
let result = task.next();
// 遞歸使用函數來保持對 next() 的調用
function step() {
// 如果還有更多要做的
if (!result.done) {
if (typeof result.value === "function") {
result.value(function(err, data) {
if (err) {
result = task.throw(err);
return;
}
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// 開始處理過程
step();
}

通過===操作符檢査後,如果result.value是一個函數,會傳入一個回調函數作為參數調用它,回調函數遵循Node.js有關執行錯誤的約定:所有可能的錯誤放在第一個參數(err)中,結果放在第二個參數中。如果傳入了err,意味著執行過程中產生了錯誤,這時通過task.throw()正確輸出錯誤對象;如果沒有錯誤產生,data被傳入task.next()作為結果儲存起來,並繼續執行step()。如果result.value不是一個函數,則直接將其傳入next()方法

現在,這個新版的任務執行器已經可以用於所有的異步任務了。在Node.js環境中,如果要從文件中讀取一些數據,需要在fs.readFile()外圍創建一個包裝器(wrapper),並返回一個與fetchData()類似的函數

let fs = require("fs");
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}

readFile()接受一個文件名作為參數,返回一個可以執行回調函數的函數。回調函數被直接傳入fs.readFile()方法,讀取完成後會執行它

run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});

在這段代碼中沒有任何回調變量,異步的readFile()操作卻正常執行,除了yield關鍵字外,其他代碼與同步代碼完全一樣,只不過函數執行的是異步操作。所以遵循相同的接口,可以編寫一些讀起來像是同步代碼的異步邏輯

當然,這些示例中使用的模式也有缺點,也就是不能百分百確認函數中返回的其他函數一定是異步的。著眼當下,最重要的是能理解任務執行過程背後的理論知識

對前端的技術,架構技術感興趣的同學關注我的頭條號,並在後臺私信發送關鍵字:“前端”即可獲取免費的架構師學習資料

知識體系已整理好,歡迎免費領取。還有面試視頻分享可以免費獲取。關注我,可以獲得沒有的架構經驗哦!!


分享到:


相關文章: