昨天講完迭代器後,今天要來討論另一種也是 ES 6 引入,且基於迭代器的實作標準:生成器(Generator)。
Generator
是 ES 6 引入的一種物件實作標準,透過 generator function
返回。
實際上 Generator
是基於迭代器規範實作的子類別,為什麼這樣說,我們來看看實際宣告的例子。
我們可以通過 function*
這樣來宣告一個 generator function
:
function* demoGenerator() {
console.log('Start');
yield 'foo';
yield 'bar';
console.log('End');
}
const gen = demoGenerator();
// "Start"
for(let val of gen){
console.log(`val : ${val}`);// val : "foo" | val : "bar"
}
console.log(gen.next()); // { value: 'foo', done: false }
console.log(gen.next()); // { value: 'bar', done: false }
// "End"
console.log(gen.next()); // { value: undefined, done: true }
可以看到昨天提到的 next()
函式在這裡也出現了,而且 gen
物件也可以透過 for of
來遍歷,所以建構生成器時返回了一個可迭代物件(iterable object),以 yield
來實現迭代器的相關訪問行為。
某種意義上可以說生成器就是一個特別應用情景的迭代器。
generator function
中,我們可以使用 yield
關鍵字來「暫停」函式的執行。
如上述例子,利用 demoGenerator()
建構了一個 gen
的 Generator
物件。
如果他是一個一般的函式,應該會在印完 "Start"
後立刻印出 "End"
,但上面的例子中,他透過 yield
暫停了函式的執行,直到對 Generator
物件調用了 .next()
才返回了 yield
關鍵字指定回傳的值,而 for of
函式也僅針對 yield
的回傳值做顯示。
生成器操作的概念上有點像狀態機,每個 yield
關鍵字的回傳就像一個狀態,透過 .next()
的呼叫來做狀態的轉移。
生成器內部可以透過 throw
語句來拋出錯誤,一旦錯誤被拋出,有兩種處理情形:
try catch
接住了,則能夠繼續進行後續的 yield
next()
function* demoGenerator() {
console.log('Start');
for(let i = 0; i < 4; i++){
try{
if(i === 1) throw Error("count to three");
else yield i;
}
catch(error){
console.log(`caught in generator : ${error.message}`);
}
}
throw Error("count to four");
yield 5;
console.log('End');
}
const gen = demoGenerator();
// "Start"
for(let val of gen){
console.log(`val : ${val}`);
}
//"val : 0"
//"caught in generator : count to three"
//"val : 2"
//"val : 3"
//"<a class='gotoLine' href='#59:9'>59:9</a> Uncaught Error: count to four"
yield*
和一般的 yield
差後面多了一個 *
,記得上面 function
加了一個 *
差在哪裡嗎?
沒錯,他會返回一個生成器物件。
function* genA() {
yield 1;
yield 2;
}
let arr = [4,5];
function* genB() {
yield* genA();
yield 3;
yield* arr;
}
const gen = genB();
for(let val of gen){
console.log(val);// 1 2 3 4 5
}
除了生成器以外,yield*
語法也能夠對可迭代物件使用(例子中的 arr
)。
這樣的語法簡化了本來如果要訪問一個可迭代物件內容做 yield
時使用 for of
的情景。
當多個生成器有可能以不同的順序互相組合時,這種語法也簡化了程式的複雜度,提高程式碼的可重用性。
昨天我們有說過一般的物件並非一個可迭代的對象,但如果我們配合使用生成器,我們就可以把物件變得可迭代。
let obj = {foo:'foo', bar:'bar'}
function* demoGenerator(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for(let val of demoGenerator(obj)){
console.log(val);//"foo" "bar"
}
這樣一個簡單的生成器可以泛用於轉換一般的物件變為可迭代,就可以對做出來的迭代器物件使用那些迭代器標準上的方法。
在迭代器裡,next()
方法僅用於按照固定的順序返回值,無法接受參數傳入。
而生成器裡面,next()
方法是可以接收傳入值的,一旦 next()
收到傳入值,便可以在生成器內部做對應的邏輯操作。
function* orderMaker() {
let order = yield "請輸入您想訂購的餐點";
yield `${order} 正在準備中`;
yield `您的 ${order} 已準備完成`;
return "感謝您的光臨"
}
let coolOrderMaker = orderMaker();
console.log(coolOrderMaker.next().value); //請輸入您想訂購的餐點
console.log(coolOrderMaker.next("咖哩飯").value); //咖哩飯 正在準備中
console.log(coolOrderMaker.next().value); // 您的 咖哩飯 已準備完成
console.log(coolOrderMaker.next()); // { value: "感謝您的光臨", done: true }
console.log(coolOrderMaker.next()); // { value: undefined, done: true }
注意賦值時機點發生在宣告賦值後下一次的 next()
呼叫。
如第一次呼叫拿到詢問,下一次呼叫時 next("咖哩飯")
存入的值會存到 order
裡。
附帶一提,如果有寫 return
語句,可以用於存到第一次觸碰到 done: true
時的 value
。當然,後續繼續嘗試用 next()
的話 value
就會變回 undefined
了,僅有第一次會有值。
生成器推出後主要的改變:
for of
等迭代器語法也相當方便yield*
語法糖對複雜結構的函式流程控制、使用都更加方便且易於閱讀next()
接收外部數據來對生成器內部做到動態的邏輯改變迭代器本身僅提供一個遍歷集合內容的方法,我們可以說生成器是一種更為強大的迭代器,更適合用於複雜邏輯的處理。
在更後面的語法出來之前,Promise
和 Generator
曾經常常被一起使用:能夠暫停函式的行為,對需要等待不確定時間的遠端呼叫再適合不過。
比如有個函式庫叫做 co,他的實踐就是透過生成器為基礎,程式碼並不多,只要看這個 檔案 就行。
展示一段使用 co
的程式碼:
function fetchData(url) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Get data from ${url}`);
}, 1000);
});
}
co(function* () {
const result1 = yield fetchData('https://api.example.com/data1');
const result2 = yield fetchData('https://api.example.com/data2');
}).catch(err => {
console.error('發生錯誤:', err);
document.getElementById('output').innerHTML = '<p>發生錯誤,請查看控制台。</p>';
});
co
允許了使用者在生成器中呼叫異步程式碼,並使用 yield
來等待其結果。
這樣就能夠保證呼叫的順序,簡化了異步的管理和避免回呼地獄,使得異步處理上更接近同步的程式碼風格。
在後續的語法出來前,Generator
加上 Promise
是那段時間裡處理異步常用的方式。