ES6+ Generator 基礎(chǔ)
1. 前言
前面我們花了三節(jié)深入地學(xué)習(xí)了 ES6 的異步解決方案 Promise,本節(jié)學(xué)習(xí)的生成器也是為了解決異步而生的,但是它的出發(fā)思路和 Promise 截然不同。
上節(jié)我們學(xué)習(xí)了 ES6 中 迭代 的相關(guān)內(nèi)容,并實(shí)現(xiàn)了一個迭代器。我們知道實(shí)現(xiàn)一個迭代器,我們需要手動添加對象的 Symbol.iterator
屬性,并需要實(shí)現(xiàn) next 方法。那么有沒有什么可以幫助我們自動實(shí)現(xiàn)迭代器呢?ES6 給出了生成器的方法來滿足我們的需求。我們不需要在對象上添加 Symbol.iterator
屬性,使用生成器函數(shù)就可以實(shí)現(xiàn)迭代器的功能。本節(jié)我們將學(xué)習(xí)生成器的相關(guān)概念和基礎(chǔ)用法。
生成器是一個靈活的結(jié)構(gòu),能使得一個函數(shù)塊內(nèi)部暫停和恢復(fù)代碼執(zhí)行的能力。在實(shí)際應(yīng)用中,使用生成器可以自定義迭代器和協(xié)程。
2. 生成器對象和生成器函數(shù)
有些概念是我們必須要理解的,前面在學(xué)習(xí)迭代器的時候,我們學(xué)習(xí)了迭代協(xié)議和迭代器協(xié)議,實(shí)現(xiàn)一個迭代器需要滿足這兩個協(xié)議才算是一個真正的迭代器。而本節(jié)的生成器和生成器函數(shù)也是如此,我們也需要知道生成器對象和生成器函數(shù)概念和它們直接的關(guān)系。
Generator 就是我們說的生成器,它包含兩個概念 生成器對象和生成器函數(shù)
。首先,要理解的是生成器對象和迭代器的關(guān)系,生成器對象是遵守迭代協(xié)議和迭代器協(xié)議實(shí)現(xiàn)的 Iterable 接口,可以理解生成器對象其實(shí)也是一個迭代器;然后,我們需要理解什么是生成器函數(shù),生成器函數(shù)是由 function *
來定義的,并且返回結(jié)果是一個 Generator 對象。
生成器是一個特殊的函數(shù),在調(diào)用后會返回一個生成器對象,這個生成器對象是遵守可迭代協(xié)議和迭代器協(xié)議實(shí)現(xiàn)的 Iterable 接口。生成器可以使用 yield 關(guān)鍵字來暫停執(zhí)行的生成器函數(shù):
function* generator() {
yield 'a';
yield 'b';
}
var gen = generator(); // Object [Generator] {}
2.1 Generator.prototype.next()
生成器的 next () 方法和迭代器返回的結(jié)果是一樣的,返回了一個包含屬性 done
和 value
的對象,該方法也可以通過接受一個參數(shù)用以向生成器傳值。
使用 yield 返回的值會被迭代器的 next () 方法捕獲:
var gen = generator();
gen.next() // {value: 'a', done: false}
gen.next() // {value: 'b', done: false}
gen.next() // {value: undefined, done: true}
從上面代碼的執(zhí)行結(jié)果可以看出,生成器函數(shù)在執(zhí)行后會返回一個生成器對象,這個生成器對象滿足迭代協(xié)議和迭代器協(xié)議,所以我們可以去手動調(diào)用它的 next () 方法去獲取每一步的返回值。從這里可以看出,生成器其實(shí)就是迭代器的一個應(yīng)用,并且這個應(yīng)用會在異步中大放異彩。
2.2 Generator.prototype.return()
return()
方法返回給定的值并結(jié)束生成器。
var gen = generator();
gen.next(); // { value: 'a', done: false }
gen.return("imooc"); // { value: "imooc", done: true }
gen.next(); // { value: undefined, done: true }
另外,如果對已經(jīng)完成狀態(tài)的生成器調(diào)用 return(value)
則生成器會一直保持在完成狀態(tài),如果出入?yún)?shù),value
會設(shè)置成傳入的參數(shù),done
的值不變:
var gen = generator();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: undefined, done: true }
gen.return(); // { value: undefined, done: true }
gen.return(1); // { value: 1, done: true }
2.2 Generator.prototype.throw()
throw()
方法用來向生成器拋出異常,并恢復(fù)生成器的執(zhí)行,返回帶有 done
及 value
兩個屬性的對象。
function* generator() {
while(true) {
try {
yield 'imooc'
} catch(e) {
console.log("Error caught!");
}
}
}
var gen = generator();
gen.next(); // { value: "imooc", done: false }
gen.throw(new Error("error")); // "Error caught!"
3. Generator 案例
3.1 類數(shù)組轉(zhuǎn)化
將一個類數(shù)組轉(zhuǎn)化為一個真正的數(shù)組方式有很多,ES6 提供了 Array.from()
可以將類數(shù)組轉(zhuǎn)化為數(shù)組 。另外在一些函數(shù)中可以使用 [...argument]
的方式轉(zhuǎn)化類數(shù)組。
function fn() {
const arg = [...arguments];
console.log(arg);
}
fn(1, 2, 3); // [1, 2, 3]
當(dāng)然我們知道類數(shù)組的定義,所以我們自己定義一個類數(shù)組,看能不能使用展開運(yùn)算符將類數(shù)組轉(zhuǎn)化為數(shù)組:
const likeArr = {
0: 1,
1: 2,
length: 2,
}
console.log([...likeArr]); // Uncaught TypeError: likeArr is not iterable
上面代碼中我們定義了一個類數(shù)組,但是使用展開運(yùn)算符報錯了,提示我們 likeArr 不是一個迭代器。因?yàn)樵诤瘮?shù)中類數(shù)組是內(nèi)部幫我們實(shí)現(xiàn)了迭代器的功能,而我們自己定義的類數(shù)組是不具有迭代器功能的,那我們來自己實(shí)現(xiàn)一個:
likeArr[Symbol.iterator] = function() {
let index = 0;
return {
next: () => {
return { value: this[index], done: index++ === this.length}
}
}
}
console.log([...likeArr]); // [1, 2]
上面的代碼我們在 likeArr 對象上定義了 Symbol.iterator
它具有迭代功能。上面代碼中我們需要手動地去實(shí)現(xiàn) next () 方法,這比較麻煩,那能不能簡化一下呢?我們的生成器函數(shù)就出場了:
likeArr[Symbol.iterator] = function* () {
let index = 0;
while (index !== this.length) {
yield this[index++];
}
}
console.log([...likeArr]); // [1, 2]
上面的代碼使用了生成器函數(shù),并且沒有去手動實(shí)現(xiàn) next () 方法,從這里我們也能很清楚地知道迭代器和生成器的關(guān)系。而且使用生成器函數(shù)更加簡單方便。
3.2 單步獲取質(zhì)數(shù)
還有一個案例是面試中經(jīng)常會考到的:
題目:實(shí)現(xiàn)一個函數(shù),每次調(diào)用返回下一個質(zhì)數(shù),要求不使用全局變量,且函數(shù)本身不接受任何參數(shù)
從題目的要求可以知道,這個函數(shù)每次調(diào)用都會返回一個質(zhì)數(shù),也就是說每次調(diào)用后都會返回一個函數(shù)。
首先我們定義一個判斷一個數(shù)是否為質(zhì)數(shù)的方法:
function isPrime(num) {
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
return false
}
}
return true
}
傳統(tǒng)的方式是使用閉包方法來解決:
function primeHandler() {
let prime = 1
return () => {
while (true) {
prime++
if (isPrime(prime)) {
return prime
}
}
}
}
const getPrime = primeHandler()
console.log(getPrime()); // 2
console.log(getPrime()); // 3
console.log(getPrime()); // 5
既然是單步執(zhí)行的,那么我們就可以使用迭代器方式實(shí)現(xiàn):
var prime = {}
prime[Symbol.iterator] = function() {
let prime = 1;
return {
next() {
while(true) {
prime++
if (isPrime(prime)) {
return prime;
}
}
}
}
}
var getPrime = prime[Symbol.iterator]().next;
console.log(getPrime()); // 2
console.log(getPrime()); // 3
上一個實(shí)例我們知道實(shí)現(xiàn)迭代器的方式是很麻煩的,可以使用生成器函數(shù)去替代迭代器的功能,所以上面的代碼可以使用生成器函數(shù)改造如下:
function* primeGenerator () {
let prime = 1
while (true) {
prime++
if (isPrime(prime)) {
yield prime
}
}
}
var getPrime = primeGenerator().next().value
console.log(getPrime()); // 2
console.log(getPrime()); // 3
4. 小結(jié)
本節(jié)我們主要學(xué)習(xí)了生成器的概念和用法,需要生成器對象是由生成器函數(shù)返回的結(jié)果,生成器對象是遵守迭代協(xié)議和迭代器協(xié)議實(shí)現(xiàn)的 Iterable 接口。生成器其實(shí)就是對迭代器的應(yīng)用。另外,通過兩個案例更加深刻地理解了生成器的應(yīng)用場景,對比了生成器和迭代器的不同。