溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

詳談ES6中的迭代器(Iterator)和生成器(Generator)

發(fā)布時間:2020-10-12 14:13:09 來源:腳本之家 閱讀:142 作者:jingxian 欄目:web開發(fā)

前面的話

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

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

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

引入

下面是一段標準的for循環(huán)代碼,通過變量i來跟蹤colors數(shù)組的索引,循環(huán)每次執(zhí)行時,如果i小于數(shù)組長度len則加1,并執(zhí)行下一次循環(huán)

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

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

迭代器

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


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

下面用ES5的語法創(chuàng)建一個迭代器

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 }"
// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"

在上面這段代碼中,createIterator()方法返回的對象有一個next()方法,每次調(diào)用時,items數(shù)組的下一個值會作為value返回。當i為3時,done變?yōu)閠rue;此時三元表達式會將value的值設(shè)置為undefined。最后兩次調(diào)用的結(jié)果與ES6迭代器的最終返回機制類似,當數(shù)據(jù)集被用盡后會返回最終的內(nèi)容

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

生成器

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

// 生成器
function *createIterator() {
 yield 1;
 yield 2;
 yield 3;
}
// 生成器能像正規(guī)函數(shù)那樣被調(diào)用,但會返回一個迭代器
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

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

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

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

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 }"
// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"

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

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

【使用限制】

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

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

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

【生成器函數(shù)表達式】

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

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 }"
// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"

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

[注意]不能用箭頭函數(shù)來創(chuàng)建生成器

【生成器對象的方法】

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

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

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

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

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

【狀態(tài)機】

生成器的一個常用功能是生成狀態(tài)機

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

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

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

for-of循環(huán)每執(zhí)行一次都會調(diào)用可迭代對象的next()方法,并將迭代器返回的結(jié)果對象的value屬性存儲在一個變量中,循環(huán)將持續(xù)執(zhí)行這一過程直到返回對象的done屬性的值為true。這里有個示例

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

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

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

[注意]如果將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獲取了數(shù)組values的默認迭代器,并用它遍歷數(shù)組中的元素。在JS引擎中執(zhí)行for-of循環(huán)語句時也會有類似的處理過程

由于具有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()函數(shù)可以檢查指定對象中是否存在默認的函數(shù)類型迭代器,而for-of循環(huán)在執(zhí)行前也會做相似的檢查

除了使用內(nèi)建的可迭代對象類型的Symbol.iterator,也可以使用Symbol.iterator來創(chuàng)建屬于自己的迭代器

【創(chuàng)建可迭代對象】

默認情況下,開發(fā)者定義的對象都是不可迭代對象,但如果給Symbol.iterator屬性添加一個生成器,則可以將其變?yōu)榭傻鷮ο?br />

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);
}

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

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

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

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

這段代碼中的展開運算符把Set集合的所有值填充到了一個數(shù)組字面量里,它可以操作所有可迭代對象,并根據(jù)默認迭代器來選取要引用的值,從迭代器讀取所有值。然后按照返回順序?qū)⑺鼈円来尾迦氲綌?shù)組中。Set集合是一個可迭代對象,展開運算符也可以用于其他可迭代對象

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

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

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

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]

創(chuàng)建一個變量allNumbers,用展開運算符將smallNumbers和bigNumbers里的值依次添加到allNumbers中。首先存入0,然后存入small中的值,最后存入bigNumbers中的值。當然,原始數(shù)組中的值只是被復制到allNumbers中,它們本身并未改變

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

內(nèi)建迭代器

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

【集合對象迭代器】

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

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

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

調(diào)用以上3個方法都可以訪問集合的迭代器

entries()迭代器

每次調(diào)用next()方法時,entries()迭代器都會返回一個數(shù)組,數(shù)組中的兩個元素分別表示集合中每個元素的鍵與值。如果被遍歷的對象是數(shù)組,則第一個元素是數(shù)字類型的索引;如果是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);
}

調(diào)用console.log()方法后輸出以下內(nèi)容

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

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

values()迭代器

調(diào)用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);
}

調(diào)用console.log()方法后輸出以下內(nèi)容

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

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

keys()迭代器

keys()迭代器會返回集合中存在的每一個鍵。如果遍歷的是數(shù)組,則會返回數(shù)字類型的鍵,數(shù)組本身的其他屬性不會被返回;如果是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);
}

調(diào)用console.log()方法后輸出以下內(nèi)容

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

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

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

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

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);
}

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

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

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

【字符串迭代器】

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

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

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


A




B

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

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

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

這段代碼輸出以下內(nèi)容


A

𠮷

B

這個結(jié)果更符合預期,通過循環(huán)語句可以直接操作字符并成功打印出Unicode字符

【NodeList迭代器】

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

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

var divs = document.getElementsByTagName("div");
for (let div of divs) {
 console.log(div.id);
}

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

高級迭代器

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

【給迭代器傳遞參數(shù)】

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

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 }"

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

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

第三次調(diào)用next()方法時,傳入數(shù)值5,這個值被賦值給second,最后用于第三條yield語句并最終返回數(shù)值8

【在迭代器中拋出錯誤】  

除了給迭代器傳遞數(shù)據(jù)外,還可以給它傳遞錯誤條件。通過throw()方法,當?shù)骰謴蛨?zhí)行時可令其拋出一個錯誤。這種主動拋出錯誤的能力對于異步編程而言至關(guān)重要,也能提供模擬結(jié)束函數(shù)執(zhí)行的兩種方法(返回值或拋出錯誤),從而增強生成器內(nèi)部的編程彈性。將錯誤對象傳給throw()方法后,在迭代器繼續(xù)執(zhí)行時其會被拋出

function *createIterator() {
 let first = yield 1;
 let second = yield first + 2; // yield 4 + 2 ,然后拋出錯誤
 yield second + 3; // 永不會被執(zhí)行
}
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"))); // 從生成器中拋出了錯誤

在這個示例中,前兩個表達式正常求值,而調(diào)用throw()方法后,在繼續(xù)執(zhí)行l(wèi)et second求值前,錯誤就會被拋出并阻止了代碼繼續(xù)執(zhí)行。這個過程與直接拋出錯誤很相似,二者唯一的區(qū)別是拋出的時機不同

可以在生成器內(nèi)部通過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語句繼續(xù)執(zhí)行后返回9

這里有一個有趣的現(xiàn)象調(diào)用throw()方法后也會像調(diào)用next()方法一樣返回一個結(jié)果對象。由于在生成器內(nèi)部捕獲了這個錯誤,因而會繼續(xù)執(zhí)行下一條yield語句,最終返回數(shù)值9

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

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

【生成器返回語句】

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

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語句將不會被執(zhí)行

在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 }"

在此示例中,第二次調(diào)用next()方法時返回對象的value屬性值為42,done屬性首次設(shè)為true;第三次調(diào)用next()方法依然返回一個對象,只是value屬性的值會變?yōu)閡ndefined。因此,通過return語句指定的返回值,只會在返回對象中出現(xiàn)一次,在后續(xù)調(diào)用返回的對象中,value屬性會被重置為undefined

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

【委托生成器】

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

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()。僅根據(jù)迭代器的返回值來看,它就像是一個完整的迭代器,可以生成所有的值。每一次調(diào)用next()方法就會委托相應的迭代器生成相應的值,直到最后由createNumberlterator()和cpeateColorlterator()創(chuàng)建的迭代器無法返回更多的值,此時執(zhí)行最后一條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()中,執(zhí)行過程先被委托給了生成器createNumberlterator(),返回值會被賦值給變量result,執(zhí)行到return 3時會返回數(shù)值3。這個值隨后被傳入createRepeatinglterator()作為它的參數(shù),因而生成字符串"repeat"的yield語句會被執(zhí)行三次

無論通過何種方式調(diào)用迭代器next()方法,數(shù)值3都不會被返回,它只存在于生成器createCombinedlterator()的內(nèi)部。但如果想輸出這個值,則可以額外添加一條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",此時將使用字符串的默認迭代器

異步任務執(zhí)行

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

執(zhí)行異步操作的傳統(tǒng)方式一般是調(diào)用一個函數(shù)并執(zhí)行相應回調(diào)函數(shù)

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

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

【簡單任務執(zhí)行器】

由于執(zhí)行yield語句會暫停當前函數(shù)的執(zhí)行過程并等待下一次調(diào)用next()方法,因此可以創(chuàng)建一個函數(shù),在函數(shù)中調(diào)用生成器生成相應的迭代器,從而在不用回調(diào)函數(shù)的基礎(chǔ)上實現(xiàn)異步調(diào)用next()方法

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

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

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

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

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

【向任務執(zhí)行器傳遞數(shù)據(jù)】

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

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

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

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

此示例會向控制臺輸出兩個數(shù)值1和4。其中,數(shù)值1取自yield 1語句中回傳給變量value的值;而4取自給變量value加3后回傳給value的值?,F(xiàn)在數(shù)據(jù)已經(jīng)能夠在yield調(diào)用間互相傳遞了,只需一個小小改變便能支持異步調(diào)用

【異步任務執(zhí)行器】

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

下面定義一個異步操作

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

本示例的原意是讓任務執(zhí)行器調(diào)用的所有函數(shù)都返回一個可以執(zhí)行回調(diào)過程的函數(shù),此處fetchData()函數(shù)的返回值是一個可接受回調(diào)函數(shù)作為參數(shù)的函數(shù),當調(diào)用它時會傳入一個字符串"Hi!"作為回調(diào)函數(shù)的參數(shù)并執(zhí)行。參數(shù)callback需要通過任務執(zhí)行器指定,以確?;卣{(diào)函數(shù)執(zhí)行時可以與底層迭代器正確交互。盡管fetchData()是同步函數(shù),但簡單添加一個延遲方法即可將其變?yōu)楫惒胶瘮?shù)

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

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

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

function run(taskDef) {
 // 創(chuàng)建迭代器,讓它在別處可用
 let task = taskDef();
 // 啟動任務
 let result = task.next();
 // 遞歸使用函數(shù)來保持對 next() 的調(diào)用
 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是一個函數(shù),會傳入一個回調(diào)函數(shù)作為參數(shù)調(diào)用它,回調(diào)函數(shù)遵循Node.js有關(guān)執(zhí)行錯誤的約定:所有可能的錯誤放在第一個參數(shù)(err)中,結(jié)果放在第二個參數(shù)中。如果傳入了err,意味著執(zhí)行過程中產(chǎn)生了錯誤,這時通過task.throw()正確輸出錯誤對象;如果沒有錯誤產(chǎn)生,data被傳入task.next()作為結(jié)果儲存起來,并繼續(xù)執(zhí)行step()。如果result.value不是一個函數(shù),則直接將其傳入next()方法

現(xiàn)在,這個新版的任務執(zhí)行器已經(jīng)可以用于所有的異步任務了。在Node.js環(huán)境中,如果要從文件中讀取一些數(shù)據(jù),需要在fs.readFile()外圍創(chuàng)建一個包裝器(wrapper),并返回一個與fetchData()類似的函數(shù)

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

readFile()接受一個文件名作為參數(shù),返回一個可以執(zhí)行回調(diào)函數(shù)的函數(shù)?;卣{(diào)函數(shù)被直接傳入fs.readFile()方法,讀取完成后會執(zhí)行它

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

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

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

以上這篇詳談ES6中的迭代器(Iterator)和生成器(Generator)就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI