對于ES6的生成器函數總結有四點:
1. yield必須放置在*函數中;
2. 每次執行到yield時都會暫停函數中剩餘代碼的執行;
3. *函數必須通過函數調用的方式(new方式會報錯)才能産生自身的執行個體,并且每個執行個體都互相獨立;
4. 一個生成器函數一旦疊代完成,則再也無法還原,一直停留在最後一個位置;
尤其是第二點,是非常強大的功能,暫停代碼執行,以前隻有在浏覽器環境中,alert、comfirm等系統内置函數才具有類似的能力,是以如果熟悉多線程的語言,你會找到類似的感覺,于是也有人說,有了yield,NodeJS就有協程的能力,完全可以處理多個需要協作的任務。
也是因為第二點,生成器函數也具有惰性求值的特性,針對這一特性,我們可以很容易寫出科裡化函數進行惰性求值,如下:
function *curr() {
let items = [],
value
do {
// 如果不需要傳回值,可以直接寫成value = yield
// 每次傳回現有的數組元素
value = yield items.slice()
// 當等于-1時終止
if(value !== -) {
items.push(value)
}
} while(value !== -)
// 進行求值
let sum =
items.forEach(item => sum = sum + item)
yield sum
}
根據生成器函數的第二個特性,隻要函數中還有yield未執行,那麼剩餘的代碼就絕不會執行,是以上面的代碼中,隻要循環未完成,求值的代碼就不會執行,再看相關的測試代碼,如下:
let curring = curr()
// 必須要空轉一次,代碼啟動柯裡化
curring.next()
curring.next()
curring.next()
curring.next()
// 啟動求值過程
console.log(curring.next(-))
上面的代碼有一次空轉的過程,這是因為next方法的參數隻可以作為上一個yield表達式的傳回值,是以對所有的生成器函數而言,都無法擷取到第一次的next()方法的傳回值,具體的執行過程如下:
1. curring.next()空轉時,執行到yield items.slice()暫停,請記住,此時還沒有傳回值,而且value的指派操作還沒有執行,請記住指派語句的右側代碼先執行;
2. 繼續執行curring.next(1),傳回值為1,進行指派操作,并繼續剩餘的循環代碼,直到遇到yield才終止本次執行;
3. 繼續執行第二步,直到方法結束為止;
在上面的執行結果中,我們還發現輸出結果有些出乎意料,如下:
{ value: , done: false }
done竟然是false,這說明方法還沒有執行結束,必須還要執行一次curring.next,才能終結方法,done才能變為true,這實在是太低效了,又空轉了一次,那怎麼改進呢?很簡單,隻需要将最後的yield改為return即可,如下:
// 進行求值
let sum =
items.forEach(item => sum = sum + item)
// 隻有使用return才能終結方法
return sum
這是yield與return的差別:
1. yield僅代表本次疊代完成,并且還必有下一次疊代;
2. return則代表生成器函數完成;
最後,為了減少柯裡化代碼中不必要的一次空轉疊代,我們用一種掩耳盜鈴的方式封裝構造函數,為什麼是掩耳盜鈴,一看就明白:
// 對生成器函數進行封裝
function wrapper(fn) {
// 這裡的...運算符将參數轉換為數組
return function(...args) {
// 除了使用析構與擴充運算,還可以使用apply函數,如下
//let generator = fn.apply(null, args)
// 這裡的...運算符是逆向運算,将數組又轉換為參數清單
let generator = fn(...args)
// 将空轉放到這裡
generator.next()
return generator;
}
}
// 現在應用代碼可以簡化了
let curring = wrapper(curr)()
curring.next()
curring.next()
curring.next()
總結
利用生成器函數可以進行惰性求值,但無法擷取到第一次next函數傳入的值,而且隻要執行了yield的傳回操作,那麼構造函數一定沒有執行完成,除非遇到了顯式的return語句。