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