前面的話
??用循環(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)
引入
下面是一段標(biāo)準(zhǔn)的
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)嵌套則需要追蹤多個變量绊率,代碼復(fù)雜度會大大增加谨敛,一不小心就錯誤使用了其他
for
循環(huán)的跟蹤變量,從而導(dǎo)致程序出錯滤否。迭代器的出現(xiàn)旨在消除這種復(fù)雜性并減少循環(huán)中的錯誤
迭代器
迭代器是一種特殊對象脸狸,它具有一些專門為迭代過程設(shè)計的專有接口,所有的迭代器對象都有一個
next()
方法,每次調(diào)用都返回一個結(jié)果對象炊甲。結(jié)果對象有兩個屬性:一個是value
泥彤,表示下一個將要返回的值;另一個是done
卿啡,它是一個布爾類型的值吟吝,當(dāng)沒有更多可返回數(shù)據(jù)時返回true
。迭代器還會保存一個內(nèi)部指針颈娜,用來指向當(dāng)前集合中值的位置剑逃,每調(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 }"http:// 之后的所有調(diào)用
console.log(iterator.next()); // "{ value: undefined, done: true }"
在上面這段代碼中乓梨,
createIterator()
方法返回的對象有一個next()
方法鳖轰,每次調(diào)用時,items
數(shù)組的下一個值會作為value
返回扶镀。當(dāng)i
為3時蕴侣,done
變?yōu)?code>true;此時三元表達式會將value
的值設(shè)置為undefined
臭觉。最后兩次調(diào)用的結(jié)果與ES6
迭代器的最終返回機制類似昆雀,當(dāng)數(shù)據(jù)集被用盡后會返回最終的內(nèi)容上面這個示例很復(fù)雜,而在
ES6
中蝠筑,迭代器的編寫規(guī)則也同樣復(fù)雜狞膘,但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ù)最有趣的部分是遭笋,每當(dāng)執(zhí)行完一條
yield
語句后函數(shù)就會自動停止執(zhí)行。舉個例子徒探,在上面這段代碼中瓦呼,執(zhí)行完語句yield
1之后,函數(shù)便不再執(zhí)行其他任何語句测暗,直到再次調(diào)用迭代器的next()
方法才會繼續(xù)執(zhí)行yield
2語句央串。生成器函數(shù)的這種中止函數(shù)執(zhí)行的能力有很多有趣的應(yīng)用使用
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 }"http:// 之后的所有調(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)部使用昌讲,在其他地方使用會導(dǎo)致程序拋出錯誤
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
語句會導(dǎo)致程序拋出語法錯誤
【生成器函數(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 }"http:// 之后的所有調(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
風(fēng)格的對象字面量中负拟,可以通過函數(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
集合)和字符串都是可迭代對象,這些對象中都有默認(rèn)的迭代器部默。ES6
中新加入的特性for-of
循環(huán)需要用到可迭代對象的這些功能
[注意]由于生成器默認(rè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,當(dāng)結(jié)果對象的done
屬性值為true
時循環(huán)退出匣椰,所以num
不會被賦值為undefined`如果只需迭代數(shù)組或集合中的值裆熙,用
for-of
循環(huán)代替for
循環(huán)是個不錯的選擇。相比傳統(tǒng)的for
循環(huán)禽笑,for-of
循環(huán)的控制條件更簡單入录,不需要追蹤復(fù)雜的條件,所以更少出錯
[注意]如果將for-of
語句用于不可迭代對象佳镜、null
或undefined
將會導(dǎo)致程序拋出錯誤
【訪問默認(rèn)迭代器】
可以通過
Symbol.iterator
來訪問對象默認(rèn)的迭代器
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
的默認(rèn)迭代器,并用它遍歷數(shù)組中的元素蟀伸。在JS
引擎中執(zhí)行for-of
循環(huán)語句時也會有類似的處理過程由于具有
Symbol.iterator
屬性的對象都有默認(rèn)的迭代器蚀同,因此可以用它來檢測對象是否為可迭代對象
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ù)可以檢查指定對象中是否存在默認(rèn)的函數(shù)類型迭代器缅刽,而for-of
循環(huán)在執(zhí)行前也會做相似的檢查除了使用內(nèi)建的可迭代對象類型的
Symbol.iterator
,也可以使用Symbol.iterator
來創(chuàng)建屬于自己的迭代器
【創(chuàng)建可迭代對象】
默認(rèn)情況下蠢络,開發(fā)者定義的對象都是不可迭代對象衰猛,但如果給
Symbol.iterator
屬性添加一個生成器,則可以將其變?yōu)榭傻鷮ο?/p>
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)建默認(rèn)的迭代器;而在生成器中芦疏,通過for-of
循環(huán)迭代this.items
并用yield
返回每一個值冕杠。collection
對象默認(rèn)迭代器的返回值由迭代器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ù)默認(rèn)迭代器來選取要引用的值,從迭代器讀取所有值薪捍。然后按照返回順序?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
集合的默認(rèn)迭代器返回的是多組鍵值對凳干,所以結(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
中的值。當(dāng)然钮追,原始數(shù)組中的值只是被復(fù)制到allNumbers
中预厌,它們本身并未改變由于展開運算符可以作用于任意可迭代對象,因此如果想將可迭代對象轉(zhuǎn)換為數(shù)組元媚,這是最簡單的方法轧叽。既可以將字符串中的每一個字符(不是編碼單元)存入新數(shù)組中,也可以將瀏覽器中
NodeList
對象中的每一個節(jié)點存入新的數(shù)組中
內(nèi)建迭代器
迭代器是
ES6
的一個重要組成部分刊棕,在ES6
中炭晒,已經(jīng)默認(rèn)為許多內(nèi)建類型提供了內(nèi)建迭代器,只有當(dāng)這些內(nèi)建迭代器無法實現(xiàn)目標(biāo)時才需要自己創(chuàng)建甥角。通常來說當(dā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ù)字類型的索引
不同集合類型的默認(rèn)迭代器
每個集合類型都有一個默認(rèn)的迭代器穴张,在
for-of
循環(huán)中,如果沒有顯式指定則使用默認(rèn)的迭代器两曼。數(shù)組和Set
集合的默認(rèn)迭代器是values()
方法皂甘,Map
集合的默認(rèn)迭代器是entries()
方法。有了這些默認(rèn)的迭代器悼凑,可以更輕松地在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);
}
- 上述代碼未指定迭代器偿枕,所以將使用默認(rèn)的迭代器璧瞬。數(shù)組、
Set
集合及Map
集合的默認(rèn)迭代器也會反應(yīng)出這些對象的初始化過程渐夸,所以這段代碼會輸出以下內(nèi)容
"red"
"green"
"blue"
1234
5678
9012["title", "Understanding ES6"]
["format", "print"]
- 默認(rèn)情況下嗤锉,如果是數(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é)果卻與預(yù)期不符
A
B
由于雙字節(jié)字符被視作兩個獨立的編碼單元生逸,從而最終在
A
與B
之間打印出4個空行所幸,
ES6
的目標(biāo)是全面支持Unicode
且预,并且我們可以通過改變字符串的默認(rèn)迭代器來解決這個問題槽袄,使其操作字符而不是編碼單元。現(xiàn)在锋谐,修改前一個示例中字符串的默認(rèn)迭代器遍尺,讓for-of
循環(huán)輸出正確的內(nèi)容
var message = "A ?? B" ;
for (let c of message) {
console.log(c);
}
- 這段代碼輸出以下內(nèi)容
A
??
B
- 這個結(jié)果更符合預(yù)期,通過循環(huán)語句可以直接操作字符并成功打印出
Unicode
字符
【NodeList迭代器】
DOM
標(biāo)準(zhǔn)中有一個NodeList
類型涮拗,document
對象中的所有元素都用這個類型來表示乾戏。對于編寫Web
瀏覽器環(huán)境中的JS
開發(fā)者來說,需要花點兒功夫去理解NodeList
對象和數(shù)組之間的差異三热。二者都使用length
屬性來表示集合中元素的數(shù)量鼓择,都可以通過方括號來訪問集合中的獨立元素。而在內(nèi)部實現(xiàn)中就漾,二者的表現(xiàn)非常不一致呐能,因而會造成很多困擾
- 自從
ES6
添加了默認(rèn)迭代器后,DOM
定義中的NodeList
類型(定義在HTML
標(biāo)準(zhǔn)而不是ES6
標(biāo)準(zhǔn)中)也擁有了默認(rèn)迭代器抑堡,其行為與數(shù)組的默認(rèn)迭代器完全一致摆出。所以可以將NodeList
應(yīng)用于for-of
循環(huán)及其他支持對象默認(rè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ǔ)功能可以輔助完成很多任務(wù)荣倾,通過生成器創(chuàng)建迭代器的過程也很便捷,除了這些簡單的集合遍歷任務(wù)之外骑丸,迭代器也可以被用于完成一些復(fù)雜的任務(wù)
【給迭代器傳遞參數(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()
方法量愧,當(dāng)?shù)骰謴?fù)執(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í)行let
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; // 當(dāng)出錯時,給變量另外賦值
}
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í)行過程集乔,當(dāng)然去件,也可以使用return
語句返回一些與普通函數(shù)返回語句不太一樣的內(nèi)容
【生成器返回語句】
由于生成器也是函數(shù),因此可以通過
return
語句提前退出函數(shù)執(zhí)行扰路,對于最后一次next()
方法調(diào)用尤溜,可以主動為其指定一個返回值。正如在其他函數(shù)中那樣汗唱,可以通過return
語句指定一個返回值宫莱。而在生成器中,return
表示所有操作已經(jīng)完成哩罪,屬性done
被設(shè)置為true
授霸;如果同時提供了相應(yīng)的值,則屬性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)?code>undefined斗搞。因此指攒,通過return
語句指定的返回值,只會在返回對象中出現(xiàn)一次僻焚,在后續(xù)調(diào)用返回的對象中允悦,value
屬性會被重置為undefined
[注意]展開運算符與for-of
循環(huán)語句會直接忽略通過return
語句指定的任何返回值,只要done
一變?yōu)?code>true就立即停止讀取其他的值虑啤。不管怎樣隙弛,迭代器的返回值依然是一個非常有用的特性
【委托生成器】
在某些情況下,我們需要將兩個迭代器合二為一狞山,這時可以創(chuàng)建一個生成器驶鹉,再給
yield
語句添加一個星號,就可以將生成數(shù)據(jù)的過程委托給其他生成器铣墨。當(dāng)定義這些生成器時,只需將星號放置在關(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()
方法就會委托相應(yīng)的迭代器生成相應(yīng)的值屡律,直到最后由createNumberlterator()
和cpeateColorlterator()
創(chuàng)建的迭代器無法返回更多的值,此時執(zhí)行最后一條yield
語句并返回true
有了生成器委托這個新功能降淮,可以進一步利用生成器的返回值來處理復(fù)雜任務(wù)
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*
也可直接應(yīng)用于字符串,例如yield*
"hello"
科盛,此時將使用字符串的默認(rèn)迭代器
異步任務(wù)執(zhí)行
生成器令人興奮的特性多與異步編程有關(guān)帽衙,
JS
中的異步編程有利有弊:簡單任務(wù)的異步化非常容易;而復(fù)雜任務(wù)的異步化會帶來很多管理代碼的挑戰(zhàn)贞绵。由于生成器支持在函數(shù)中暫停代碼執(zhí)行厉萝,因而可以深入挖掘異步處理的更多用法
- 執(zhí)行異步操作的傳統(tǒng)方式一般是調(diào)用一個函數(shù)并執(zhí)行相應(yīng)回調(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í)行的任務(wù)很少,那么這樣的方式可以很好地完成任務(wù)漓藕;如若需要嵌套回調(diào)或序列化一系列的異步操作嚎幸,事情會變得非常復(fù)雜。此時花沉,生成器和yield
語句就派上用場了
【簡單任務(wù)執(zhí)行器】
由于執(zhí)行
yield
語句會暫停當(dāng)前函數(shù)的執(zhí)行過程并等待下一次調(diào)用next()
方法,因此可以創(chuàng)建一個函數(shù),在函數(shù)中調(diào)用生成器生成相應(yīng)的迭代器舞肆,從而在不用回調(diào)函數(shù)的基礎(chǔ)上實現(xiàn)異步調(diào)用next()
方法
function run(taskDef) {
// 創(chuàng)建迭代器,讓它在別處可用
let task = taskDef();// 啟動任務(wù)
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í)行的任務(wù)椿胯,生成一個迭代器并將它儲存在變量task
中。首次調(diào)用迭代器的next()
方法時剃根,返回的結(jié)果被儲存起來稍后繼續(xù)使用哩盲。step()
函數(shù)會檢查result.done
的值,如果為false
則執(zhí)行迭代器的next()
方法狈醉,并再次執(zhí)行step()
操作廉油。每次調(diào)用next()
方法時,返回的最新信息總會覆寫變量result
苗傅。在代碼的最后抒线,初始化執(zhí)行step()
函數(shù)并開始整個的迭代過程,每次通過檢查result.done
來確定是否有更多任務(wù)需要執(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逊桦。當(dāng)然眨猎,簡單輸出迭代次數(shù)不足以展示迭代器高級功能的實用之處,下一步將在迭代器與調(diào)用者之間互相傳值
【向任務(wù)執(zhí)行器傳遞數(shù)據(jù)】
給任務(wù)執(zhí)行器傳遞數(shù)據(jù)的最簡單辦法是卫袒,將值通過迭代器的
next()
方法傳入作為yield
的生成值供下次調(diào)用宵呛。在這段代碼中,只需將result.value
傳入next()
方法即可
function run(taskDef) {// 創(chuàng)建迭代器夕凝,讓它在別處可用
let task = taskDef();// 啟動任務(wù)
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
的值。現(xiàn)在數(shù)據(jù)已經(jīng)能夠在yield
調(diào)用間互相傳遞了转砖,只需一個小小改變便能支持異步調(diào)用
【異步任務(wù)執(zhí)行器】
之前的示例只是在多個
yield
調(diào)用間來回傳遞靜態(tài)數(shù)據(jù)须鼎,而等待一個異步過程有些不同鲸伴。任務(wù)執(zhí)行器需要知曉回調(diào)函數(shù)是什么以及如何使用它。由于yield
表達式會將值返回給任務(wù)執(zhí)行器晋控,所有的函數(shù)調(diào)用都會返回一個值汞窗,因而在某種程度上這也是一個異步操作,任務(wù)執(zhí)行器會一直等待直到操作完成
- 下面定義一個異步操作
function fetchData() {
return function(callback) {
callback(null, "Hi!");
};
}
- 本示例的原意是讓任務(wù)執(zhí)行器調(diào)用的所有函數(shù)都返回一個可以執(zhí)行回調(diào)過程的函數(shù)赡译,此處
fetchData()
函數(shù)的返回值是一個可接受回調(diào)函數(shù)作為參數(shù)的函數(shù)仲吏,當(dāng)調(diào)用它時會傳入一個字符串"Hi!"
作為回調(diào)函數(shù)的參數(shù)并執(zhí)行。參數(shù)callback
需要通過任務(wù)執(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ù)中異步過程的運作方式成畦,可以將任務(wù)執(zhí)行器稍作修改。當(dāng)
result.value
是一個函數(shù)時涝开,任務(wù)執(zhí)行器會先執(zhí)行這個函數(shù)再將結(jié)果傳入next()
方法
function run(taskDef) {
// 創(chuàng)建迭代器羡鸥,讓它在別處可用
let task = taskDef();// 啟動任務(wù)
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)在宪祥,這個新版的任務(wù)執(zhí)行器已經(jīng)可以用于所有的異步任務(wù)了聂薪。在
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í)行的是異步操作撮抓。所以遵循相同的接口妇斤,可以編寫一些讀起來像是同步代碼的異步邏輯當(dāng)然,這些示例中使用的模式也有缺點胀滚,也就是不能百分百確認(rèn)函數(shù)中返回的其他函數(shù)一定是異步的趟济。著眼當(dāng)下,最重要的是能理解任務(wù)執(zhí)行過程背后的理論知識
其他章節(jié)
- ES6-數(shù)字?jǐn)U展
- ES6-字符串拓展
- ES6-模板字面量
- ES6-關(guān)于Unicode的相關(guān)擴展
- ES6-正則表達式擴展
- ES6-函數(shù)擴展
- ES6-對象擴展
- ES6-Symbol
- ES6-Set和Map集合
- ES6-數(shù)組擴展
- ES6-定型數(shù)組
- ES6-塊級作用域
- ES6-解構(gòu)賦值
- ES6-類
- ES6-代理(Proxy)和反射(Reflection)
- ES6-ES6中的模塊
- ES6-ES2017中的修飾器Decorator
- ES6-迭代器(Iterator)和生成器(Generator)
- ES6-Promise和異步編程
- ES6-ES2017中的async