摘要:我們前面說過生成器函數(shù)本身永遠(yuǎn)返回一個迭代器,而生成器中的語句實際上是關(guān)閉迭代器的標(biāo)志,實際代表。生成器函數(shù)返回的迭代器本身就是一個對象,很容易想到改變對象的原型實現(xiàn)。
之前的文章 寫到了 Generator 與異步編程的關(guān)系,其實簡化異步編程只是 Generator 的“副業(yè)”,Generator 本身卻不是為異步編程而存在。
生成器函數(shù)我們看 Generator 自身的含義——生成器,就是產(chǎn)生序列用的。比如有如下函數(shù):
function* range(start, stop) { for (let item = start; item < stop; ++item) { yield item; } }
range 就是一個生成器函數(shù),它自身是函數(shù)可以調(diào)用(typeof range === "function" // true),但又與普通函數(shù)不同,生成器函數(shù)(GeneratorFunction)永遠(yuǎn)返回一個生成器(Generator)
注:我們通常所說的 Generator 實際上指生成器函數(shù)(GeneratorFunction),而把生成器函數(shù)返回的對象稱作迭代器(Iterator)。由于感覺“生成器函數(shù)”返回“生成器”這句話有些拗口,下文沿用生成器和迭代器的說法。
迭代器初次調(diào)用生成器實際上不執(zhí)行生成器函數(shù)的函數(shù)體,它只是返回一個迭代器,當(dāng)用戶調(diào)用迭代器的 next 函數(shù)時,程序才開始真正執(zhí)行生成器的函數(shù)體。當(dāng)程序運行到 yield 表達式時,會將 yield 后面表達式的值作為 next 函數(shù)的返回值(的一部分)返回,函數(shù)本身暫停執(zhí)行。
const iterator = range(0, 10); // 獲取迭代器 const value1 = iterator.next().value; // 獲取第一個值 => 0 const value2 = iterator.next().value; // 獲取第二個值 => 1
next 返回值是一個對象,包含兩個屬性 value 和 done。value 即為 yield 后面表達式的值,done 表示函數(shù)是否已經(jīng)結(jié)束(return)。如果函數(shù) return(或執(zhí)行到函數(shù)尾退出,相當(dāng)于 return undefined),則 done 為 true,value 為 return 的值。
for...of 是遍歷整個迭代器的簡單方式。
生成器的用處上面說到,生成器就是生成序列用的。但是與直接返回數(shù)組不同,生成器返回序列是一項一項計算并返回的,而返回數(shù)組總是需要計算出所有值后統(tǒng)一返回。所以至少有三種情況應(yīng)該考慮使用生成器。
序列有無限多項,或者調(diào)用者不確定需要多少項
range(0, Infinity) 是允許的,因為生成器沒生成一個值就會暫停執(zhí)行,所以不會造成死循環(huán),可以由調(diào)用者選擇何時停止。
注意此時不能使用 for...of,因為迭代器永遠(yuǎn)不會 done
計算每一項耗時較長
如果計算一項的值需要 1ms,那么計算 1000 項就需要 1s,如果不將這 1s 拆分,就會導(dǎo)致瀏覽器卡頓甚至假死。這時可以用生成器每生成幾項就將控制權(quán)交還給瀏覽器,用于響應(yīng)用戶事件,提升用戶體驗(當(dāng)然這里更有效的方法是將代碼放到 Web Worker 里執(zhí)行)
節(jié)省內(nèi)存
如果序列很長,直接返回數(shù)組會占用較大內(nèi)存。生成器返回值是一項一項返回,不會一次性占用大量內(nèi)存(當(dāng)然生成器為了保留執(zhí)行上下文比通常函數(shù)占用內(nèi)存更多,但是它是個定值,不隨迭代次數(shù)增加)
使用生成器實現(xiàn)懶加載的 map、filterArray#map、Array#filter 是 ES5 引入的(絕對不算新的)兩個非常常用的函數(shù),前者將數(shù)組每一項通過回調(diào)函數(shù)映射到新數(shù)組(值變量不變),后者通過回調(diào)函數(shù)過濾某些不需要的項(量變值不變),他們都會生成新的數(shù)組對象,如果數(shù)組本身較長或者寫了很長的 map、filter 調(diào)用鏈,就可能造成內(nèi)存浪費。
這時就可以考慮使用生成器實現(xiàn)這兩個函數(shù),非常簡單:
function* map(iterable, callback) { let i = 0; for (const item of iterable) { // 遍歷數(shù)組 yield callback(item, i++, iterable); // 獲取其中一項,調(diào)用回調(diào)函數(shù),yield 返回值 } } function* filter(iterable, callback) { let i = 0; for (const item of iterable) { // 遍歷數(shù)組 if (callback(item, i++, iterable)) { // 獲取其中一項,調(diào)用回調(diào)函數(shù) yield item; // 僅當(dāng)回調(diào)函數(shù)返回真時,才 yield 值 } } }
可以看到我在代碼中寫的是“可迭代的”(iterable),而不限于數(shù)組(由于實現(xiàn)了 Symbol.iterator 所以數(shù)組也是可迭代對象)。比如可以這么用:
const result = map( // (1) filter( // (2) range(1, 10000), // (3) x => x % 2 === 0, ), x => x / 2, ) console.log(...result); // (4)
注意,程序在解構(gòu)運算符 ...result 這一步才真正開始計算 result 的值(所謂的懶加載),而且它的值也是一個一個計算的:
(3)生成迭代器,提供值給(2);(2)提供值給(1)
(1)中的result也是迭代器,在這一步所有函數(shù)都沒有真正開始執(zhí)行,因為沒有任何代碼問他們索要值。
(4)中的擴展運算符對迭代器 result 進行了求值,生成器真正開始執(zhí)行。
result 的值來自于 (1),于是(1)首先開始執(zhí)行。
(1)中map函數(shù)使用 for...of 遍歷(2)提供的迭代器,于是(2)開始執(zhí)行
(2)中filter函數(shù)使用 for...of 遍歷(3)提供的迭代器,于是(3)開始執(zhí)行
(3)中range函數(shù)開始執(zhí)行,循環(huán)得到第一個值 1。遇到 yield 關(guān)鍵字,將值 1 輸出給(2)
(2)中的 for...of 獲得一個值 1,執(zhí)行函數(shù)體。callback 返回 false,忽略之。回到 for...of,繼續(xù)問(3)索要下一個值
(3)range 輸出第二個值 2
(2)中的 for...of 獲得一個值 2,執(zhí)行函數(shù)體。callback 返回 true,將值 2 輸出給 (1)
(1)中的 for...of 獲得一個值 2,執(zhí)行函數(shù)體得到 1。將值 1 輸出給(4),console.log 獲得第一個參數(shù)
(4)檢測result還沒有結(jié)束(done為false),問result索要下一個值。
回到第 4 步循環(huán),直至(3)中的循環(huán)結(jié)束函數(shù)體退出為止,(3)返回的迭代器被關(guān)閉
(2)中 for...of 檢測到迭代器已被關(guān)閉(done為true),循環(huán)結(jié)束,函數(shù)退出,(2)返回的迭代器被關(guān)閉
同理(1)返回的迭代器被關(guān)閉
(4)中解構(gòu)運算符檢測到result已關(guān)閉,結(jié)構(gòu)結(jié)束。將結(jié)構(gòu)得到的所有值作為 console.log 的參數(shù)列表輸出
總結(jié)一下,代碼執(zhí)行順序大概是這樣:(3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> (2) -> (3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> ……
是不是相當(dāng)復(fù)雜?異步函數(shù)中“跳來跳去”的執(zhí)行順序也就是這么來的。跟遞歸函數(shù)一樣,不要太糾結(jié)生成器函數(shù)的執(zhí)行順序,而要著重理解它這一步究竟要做什么事情。
生成器函數(shù)的鏈?zhǔn)秸{(diào)用這樣的代碼 map(filter(range(1, 100), x => x % 2 === 0), x => x / 2) 似乎有些d疼,好像又看到了被回調(diào)函數(shù)支配的恐懼。雖然有人提出了管道符的提議,但這種 stage 1 的提議被標(biāo)準(zhǔn)接受直至有瀏覽器實現(xiàn)實在是遙遙無期,有沒有其他辦法呢?
普通函數(shù)可以簡單的通過在類中返回 this 實現(xiàn)函數(shù)的鏈?zhǔn)秸{(diào)用(例如經(jīng)典的 jQuery),但是這點在生成器中不適用。我們前面說過生成器函數(shù)本身永遠(yuǎn)返回一個迭代器,而生成器中的 return 語句實際上是關(guān)閉迭代器的標(biāo)志,return this 實際代表 { value: this, done: true }。生成器中的 return 和普通函數(shù)用法相近但實際含義大大不同。
鏈?zhǔn)秸{(diào)用需要函數(shù)的返回值是個對象,并且對象中包含可鏈?zhǔn)秸{(diào)用的所有函數(shù)。生成器函數(shù)返回的迭代器本身就是一個對象,很容易想到改變對象的原型實現(xiàn)。
迭代器有如下原型繼承鏈:
迭代器對象 -> 生成器.prototype -> 生成器.prototype.prototype(Generator) -> Object.prototype -> null
可以看到,生成器返回的迭代器對象就好像是被生成器 new 出來的一樣(但是生成器不是構(gòu)造函數(shù)不能被 new)。但是總之我們可以通過給生成器函數(shù)的 prototype 添加方法實現(xiàn)給迭代器添加方法的效果。實現(xiàn)如下
function* range(start, stop) { for (let item = start; item < stop; ++item) { yield item; } } function* map(callback) { let i = 0; for (const item of this) { yield callback(item, i++, this); } } function* filter(callback) { let i = 0; for (const item of this) { if (callback(item, i++, this)) { yield item; } } } [range, map, filter].forEach(x => Object.assign(x.prototype, { range, map, filter })); // 使用 const result = range(1, 100) .filter(x => x % 2 === 0) .map(x => x / 2); console.log(...result);完
筆者業(yè)(xian)余(de)時(dan)間(teng)使用迭代器實現(xiàn)了幾乎所有 ES7 中 Array 的成員方法和靜態(tài)方法,廣而告之,歡迎來噴:https://github.com/CarterLi/q...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/103737.html
摘要:示例代碼如下此示例中可以看出,當(dāng)?shù)鹘K止時,通過拋出異常告知迭代器已耗盡。但如果迭代器所指向的數(shù)據(jù)結(jié)構(gòu)在其存在時發(fā)生了插入或刪除操作,則迭代器將可能失效。與的情形類似,對進行任何插入操作也將損壞迭代器。 花下貓語:之前說過,我對于編程語言跟其它學(xué)科的融合非常感興趣,但我還說漏了一點,就是我對于 Python 跟其它編程語言的對比學(xué)習(xí),也很感興趣。所以,我一直希望能聚集一些有其它語言基...
摘要:簡評迭代器是惰性可迭代對象,函數(shù)在中是一個惰性的可迭代對象,那么是不是迭代器呢為什么。如果你不能將某些東西傳遞給函數(shù),那么它不是一個迭代器。的對象不是迭代器。 簡評:迭代器(iterator)是惰性可迭代對象(lazy iterable),range 函數(shù)在 Python 3 中是一個惰性的可迭代對象,那么 range 是不是迭代器呢?為什么。 TLNR:Python 3 中的 ran...
摘要:什么是迭代器中創(chuàng)建迭代器如下所示什么是生成器生成器是一種返回迭代器的函數(shù)每當(dāng)招待完一條語句后函數(shù)就會自動停止執(zhí)行關(guān)鍵字可返回任何值或表達式關(guān)鍵字只可在生成器內(nèi)部使用,在其它地方使用會導(dǎo)致程序拋出語法錯誤所以下面例子是有錯誤的可迭代對象具有屬 什么是迭代器 ES5中創(chuàng)建迭代器如下所示: function createIterator(items) { var i = 0 retu...
閱讀 1860·2021-11-15 11:39
閱讀 1226·2021-10-18 13:29
閱讀 1187·2021-08-31 09:42
閱讀 2741·2019-08-30 11:11
閱讀 2116·2019-08-26 12:12
閱讀 2116·2019-08-26 10:17
閱讀 3391·2019-08-23 18:38
閱讀 3228·2019-08-23 18:38