W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
常規(guī)函數(shù)只會(huì)返回一個(gè)單一值(或者不返回任何值)。
而 generator 可以按需一個(gè)接一個(gè)地返回(“yield”)多個(gè)值。它們可與 iterable 完美配合使用,從而可以輕松地創(chuàng)建數(shù)據(jù)流。
要?jiǎng)?chuàng)建一個(gè) generator,我們需要一個(gè)特殊的語(yǔ)法結(jié)構(gòu):function*
,即所謂的 “generator function”。
它看起來(lái)像這樣:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
generator 函數(shù)與常規(guī)函數(shù)的行為不同。在此類函數(shù)被調(diào)用時(shí),它不會(huì)運(yùn)行其代碼。而是返回一個(gè)被稱為 “generator object” 的特殊對(duì)象,來(lái)管理執(zhí)行流程。
我們來(lái)看一個(gè)例子:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" 創(chuàng)建了一個(gè) "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
到目前為止,上面這段代碼中的 函數(shù)體 代碼還沒有開始執(zhí)行:
一個(gè) generator 的主要方法就是 next()
。當(dāng)被調(diào)用時(shí)(譯注:指 next()
方法),它會(huì)恢復(fù)上圖所示的運(yùn)行,執(zhí)行直到最近的 yield <value>
語(yǔ)句(value
可以被省略,默認(rèn)為 undefined
)。然后函數(shù)執(zhí)行暫停,并將產(chǎn)出的(yielded)值返回到外部代碼。
next()
的結(jié)果始終是一個(gè)具有兩個(gè)屬性的對(duì)象:
value
?: 產(chǎn)出的(yielded)的值。done
?: 如果 generator 函數(shù)已執(zhí)行完成則為 ?true
?,否則為 ?false
?。例如,我們可以創(chuàng)建一個(gè) generator 并獲取其第一個(gè)產(chǎn)出的(yielded)值:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
截至目前,我們只獲得了第一個(gè)值,現(xiàn)在函數(shù)執(zhí)行處在第二行:
讓我們?cè)俅握{(diào)用 generator.next()
。代碼恢復(fù)執(zhí)行并返回下一個(gè) yield
的值:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
如果我們第三次調(diào)用 generator.next()
,代碼將會(huì)執(zhí)行到 return
語(yǔ)句,此時(shí)就完成這個(gè)函數(shù)的執(zhí)行:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
現(xiàn)在 generator 執(zhí)行完成。我們通過 done:true
可以看出來(lái)這一點(diǎn),并且將 value:3
處理為最終結(jié)果。
再對(duì) generator.next()
進(jìn)行新的調(diào)用不再有任何意義。如果我們這樣做,它將返回相同的對(duì)象:{done: true}
。
?
function* f(…)
? 或 ?function *f(…)
??這兩種語(yǔ)法都是對(duì)的。
但是通常更傾向于第一種語(yǔ)法,因?yàn)樾翘?hào)
*
表示它是一個(gè) generator 函數(shù),它描述的是函數(shù)種類而不是名稱,因此*
應(yīng)該和function
關(guān)鍵字緊貼一起。
當(dāng)你看到 next()
方法,或許你已經(jīng)猜到了 generator 是 可迭代(iterable)的。
我們可以使用 for..of
循環(huán)遍歷它所有的值:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1,然后是 2
}
for..of
寫法是不是看起來(lái)比 .next().value
優(yōu)雅多了?
……但是請(qǐng)注意:上面這個(gè)例子會(huì)先顯示 1
,然后是 2
,然后就沒了。它不會(huì)顯示 3
!
這是因?yàn)楫?dāng) done: true
時(shí),for..of
循環(huán)會(huì)忽略最后一個(gè) value
。因此,如果我們想要通過 for..of
循環(huán)顯示所有的結(jié)果,我們必須使用 yield
返回它們:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1,然后是 2,然后是 3
}
因?yàn)?generator 是可迭代的,我們可以使用 iterator 的所有相關(guān)功能,例如:spread 語(yǔ)法 ...
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
在上面這段代碼中,...generateSequence()
將可迭代的 generator 對(duì)象轉(zhuǎn)換為了一個(gè)數(shù)組(關(guān)于 spread 語(yǔ)法的更多細(xì)節(jié)請(qǐng)見 Rest 參數(shù)與 Spread 語(yǔ)法)。
在前面的 Iterable object(可迭代對(duì)象) 一章中,我們創(chuàng)建了一個(gè)可迭代的 range
對(duì)象,它返回 from..to
的值。
現(xiàn)在,我們回憶一下代碼:
let range = {
from: 1,
to: 5,
// for..of range 在一開始就調(diào)用一次這個(gè)方法
[Symbol.iterator]() {
// ...它返回 iterator object:
// 后續(xù)的操作中,for..of 將只針對(duì)這個(gè)對(duì)象,并使用 next() 向它請(qǐng)求下一個(gè)值
return {
current: this.from,
last: this.to,
// for..of 循環(huán)在每次迭代時(shí)都會(huì)調(diào)用 next()
next() {
// 它應(yīng)該以對(duì)象 {done:.., value :...} 的形式返回值
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// 迭代整個(gè) range 對(duì)象,返回從 `range.from` 到 `range.to` 范圍的所有數(shù)字
alert([...range]); // 1,2,3,4,5
我們可以通過提供一個(gè) generator 函數(shù)作為 Symbol.iterator
,來(lái)使用 generator 進(jìn)行迭代:
下面是一個(gè)相同的 range
,但緊湊得多:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的簡(jiǎn)寫形式
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
之所以代碼正常工作,是因?yàn)?nbsp;range[Symbol.iterator]()
現(xiàn)在返回一個(gè) generator,而 generator 方法正是 for..of
所期望的:
.next()
? 方法{value: ..., done: true/false}
? 的形式返回值當(dāng)然,這不是巧合。generator 被添加到 JavaScript 語(yǔ)言中是有對(duì) iterator 的考量的,以便更容易地實(shí)現(xiàn) iterator。
帶有 generator 的變體比原來(lái)的 range
迭代代碼簡(jiǎn)潔得多,并且保持了相同的功能。
generator 可以永遠(yuǎn)產(chǎn)出(yield)值
在上面的示例中,我們生成了有限序列,但是我們也可以創(chuàng)建一個(gè)生成無(wú)限序列的 generator,它可以一直產(chǎn)出(yield)值。例如,無(wú)序的偽隨機(jī)數(shù)序列。
這種情況下肯定需要在 generator 的
for..of
循環(huán)中添加一個(gè)break
(或者return
)。否則循環(huán)將永遠(yuǎn)重復(fù)下去并掛起。
generator 組合(composition)是 generator 的一個(gè)特殊功能,它允許透明地(transparently)將 generator 彼此“嵌入(embed)”到一起。
例如,我們有一個(gè)生成數(shù)字序列的函數(shù):
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
現(xiàn)在,我們想重用它來(lái)生成一個(gè)更復(fù)雜的序列:
0..9
?(字符代碼為 48…57),A..Z
?(字符代碼為 65…90)a...z
?(字符代碼為 97…122)我們可以對(duì)這個(gè)序列進(jìn)行應(yīng)用,例如,我們可以從這個(gè)序列中選擇字符來(lái)創(chuàng)建密碼(也可以添加語(yǔ)法字符),但讓我們先生成它。
在常規(guī)函數(shù)中,要合并其他多個(gè)函數(shù)的結(jié)果,我們需要調(diào)用它們,存儲(chǔ)它們的結(jié)果,最后再將它們合并到一起。
對(duì)于 generator 而言,我們可以使用 yield*
這個(gè)特殊的語(yǔ)法來(lái)將一個(gè) generator “嵌入”(組合)到另一個(gè) generator 中:
組合的 generator 的例子:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield*
指令將執(zhí)行 委托 給另一個(gè) generator。這個(gè)術(shù)語(yǔ)意味著 yield* gen
在 generator gen
上進(jìn)行迭代,并將其產(chǎn)出(yield)的值透明地(transparently)轉(zhuǎn)發(fā)到外部。就好像這些值就是由外部的 generator yield 的一樣。
執(zhí)行結(jié)果與我們內(nèi)聯(lián)嵌套 generator 中的代碼獲得的結(jié)果相同:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
generator 組合(composition)是將一個(gè) generator 流插入到另一個(gè) generator 流的自然的方式。它不需要使用額外的內(nèi)存來(lái)存儲(chǔ)中間結(jié)果。
目前看來(lái),generator 和可迭代對(duì)象類似,都具有用來(lái)生成值的特殊語(yǔ)法。但實(shí)際上,generator 更加強(qiáng)大且靈活。
這是因?yàn)?nbsp;yield
是一條雙向路(two-way street):它不僅可以向外返回結(jié)果,而且還可以將外部的值傳遞到 generator 內(nèi)。
調(diào)用 generator.next(arg)
,我們就能將參數(shù) arg
傳遞到 generator 內(nèi)部。這個(gè) arg
參數(shù)會(huì)變成 yield
的結(jié)果。
我們來(lái)看一個(gè)例子:
function* gen() {
// 向外部代碼傳遞一個(gè)問題并等待答案
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield 返回的 value
generator.next(4); // --> 將結(jié)果傳遞到 generator 中
generator.next()
? 應(yīng)該是不帶參數(shù)的(如果帶參數(shù),那么該參數(shù)會(huì)被忽略)。它開始執(zhí)行并返回第一個(gè) ?yield "2 + 2 = ?"
? 的結(jié)果。此時(shí),generator 執(zhí)行暫停,而停留在 (?*
?) 行上。yield
? 的結(jié)果進(jìn)入調(diào)用代碼中的 ?question
? 變量。generator.next(4)
?,generator 恢復(fù)執(zhí)行,并獲得了 ?4
? 作為結(jié)果:?let result = 4
?。請(qǐng)注意,外部代碼不必立即調(diào)用 next(4)
。外部代碼可能需要一些時(shí)間。這沒問題:generator 將等待它。
例如:
// 一段時(shí)間后恢復(fù) generator
setTimeout(() => generator.next(4), 1000);
我們可以看到,與常規(guī)函數(shù)不同,generator 和調(diào)用 generator 的代碼可以通過在 next/yield
中傳遞值來(lái)交換結(jié)果。
為了講得更淺顯易懂,我們來(lái)看另一個(gè)例子,其中包含了許多調(diào)用:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
執(zhí)行圖:
.next()
? 啟動(dòng)了 generator 的執(zhí)行……執(zhí)行到達(dá)第一個(gè) ?yield
?。.next(4)
? 將 4 作為第一個(gè) ?yield
? 的結(jié)果傳遞回 generator 并恢復(fù) generator 的執(zhí)行。yield
?,它變成了 generator 調(diào)用的結(jié)果。next(9)
? 將 ?9
? 作為第二個(gè) ?yield
? 的結(jié)果傳入 generator 并恢復(fù) generator 的執(zhí)行,執(zhí)行現(xiàn)在到達(dá)了函數(shù)的最底部,所以返回 ?done: true
?。這個(gè)過程就像“乒乓球”游戲。每個(gè) next(value)
(除了第一個(gè))傳遞一個(gè)值到 generator 中,該值變成了當(dāng)前 yield
的結(jié)果,然后獲取下一個(gè) yield
的結(jié)果。
正如我們?cè)谏厦娴睦又杏^察到的那樣,外部代碼可能會(huì)將一個(gè)值傳遞到 generator,作為 yield
的結(jié)果。
……但是它也可以在那里發(fā)起(拋出)一個(gè) error。這很自然,因?yàn)?error 本身也是一種結(jié)果。
要向 yield
傳遞一個(gè) error,我們應(yīng)該調(diào)用 generator.throw(err)
。在這種情況下,err
將被拋到對(duì)應(yīng)的 yield
所在的那一行。
例如,"2 + 2?"
的 yield 導(dǎo)致了一個(gè) error:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // 顯示這個(gè) error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
在 (2)
行引入到 generator 的 error 導(dǎo)致了在 (1)
行中的 yield
出現(xiàn)了一個(gè)異常。在上面這個(gè)例子中,try..catch
捕獲并顯示了這個(gè) error。
如果我們沒有捕獲它,那么就會(huì)像其他的異常一樣,它將從 generator “掉出”到調(diào)用代碼中。
調(diào)用代碼的當(dāng)前行是 generator.throw
所在的那一行,標(biāo)記為 (2)
。所以我們可以在這里捕獲它,就像這樣:
function* generate() {
let result = yield "2 + 2 = ?"; // 這行出現(xiàn) error
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // 顯示這個(gè) error
}
如果我們沒有在那里捕獲這個(gè) error,那么,通常,它會(huì)掉入外部調(diào)用代碼(如果有),如果在外部也沒有被捕獲,則會(huì)殺死腳本。
generator.return(value)
完成 generator 的執(zhí)行并返回給定的 value
。
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }
如果我們?cè)谝淹瓿傻?generator 上再次使用 generator.return()
,它將再次返回該值(MDN)。
通常我們不使用它,因?yàn)榇蠖鄶?shù)時(shí)候我們想要獲取所有的返回值,但是當(dāng)我們想要在特定條件下停止 generator 時(shí)它會(huì)很有用。
function* f(…) {…}
? 創(chuàng)建的。yield
? 操作。next/yield
? 調(diào)用交換結(jié)果。在現(xiàn)代 JavaScript 中,generator 很少被使用。但有時(shí)它們會(huì)派上用場(chǎng),因?yàn)楹瘮?shù)在執(zhí)行過程中與調(diào)用代碼交換數(shù)據(jù)的能力是非常獨(dú)特的。而且,當(dāng)然,它們非常適合創(chuàng)建可迭代對(duì)象。
并且,在下一章我們將會(huì)學(xué)習(xí) async generator,它們被用于在 for await ... of
循環(huán)中讀取異步生成的數(shù)據(jù)流(例如,通過網(wǎng)絡(luò)分頁(yè)提取 (paginated fetches over a network))。
在 Web 編程中,我們經(jīng)常使用數(shù)據(jù)流,因此這是另一個(gè)非常重要的使用場(chǎng)景。
在很多地方我們都需要隨機(jī)數(shù)據(jù)。
其中之一就是測(cè)試。我們可能需要隨機(jī)數(shù)據(jù):文本,數(shù)字等,以便很好地進(jìn)行測(cè)試。
在 JavaScript 中,我們可以使用 Math.random()
。但是如果什么地方出現(xiàn)了問題,我們希望能使用完全相同的數(shù)據(jù)進(jìn)行重復(fù)測(cè)試。
為此,我們可以使用所謂的“種子偽隨機(jī)(seeded pseudo-random)generator”。它們將“種子(seed)”作為第一個(gè)值,然后使用公式生成下一個(gè)值。以便相同的種子(seed)可以產(chǎn)出(yield)相同的序列,因此整個(gè)數(shù)據(jù)流很容易復(fù)現(xiàn)。我們只需要記住種子并重復(fù)它即可。
這樣的公式的一個(gè)示例如下,它可以生成一些均勻分布的值:
next = previous * 16807 % 2147483647
如果我們使用 1
作為種子,生成的值將會(huì)是:
16807
?282475249
?1622650073
?這里的任務(wù)是創(chuàng)建一個(gè) generator 函數(shù) pseudoRandom(seed)
,它將 seed
作為參數(shù)并使用此公式創(chuàng)建 generator。
使用范例:
let generator = pseudoRandom(1);
alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
function* pseudoRandom(seed) {
let value = seed;
while(true) {
value = value * 16807 % 2147483647
yield value;
}
};
let generator = pseudoRandom(1);
alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
請(qǐng)注意,也可以使用常規(guī)函數(shù)來(lái)完成相同的操作,就像這樣:
function pseudoRandom(seed) {
let value = seed;
return function() {
value = value * 16807 % 2147483647;
return value;
}
}
let generator = pseudoRandom(1);
alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073
這也可以工作。但是這樣我們就失去了使用 for..of
來(lái)進(jìn)行迭代以及使用 generator 組合(composition)的能力,這些可能在其他地方很有用。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: