一、什么是生成器 Generator?
生成器對象是由一個 Generator 函數返回的,并且她符合 可迭代協議和迭代器協議。
語法:
function * gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"
生成器函數有以下四個特征:
- 在 function 關鍵字和函數名之間多了一個星號
- 函數內部使用了 yield 表達式,用于定義 Generator 函數中的每個狀態
- Generator 函數通過多個 yield 表達式定義內部狀態,掉用 Generator 函數時,不會像普通函數會立即執行,而是返回的是一個 Iterator 對象,通過調用 next() 方法,可以依次遍歷 Generator 函數的每個內部狀態
- Generator 函數的星號的位置有四種情況 如下
function *gen () {}
function* gen () {}
function * gen () {}
function*gen () {}
一般的寫法是第二種。
二、如何使用 Generator?
1、yield 表達式
yield 關鍵字在 Generator 函數中有兩個作用:定義內部狀態和暫停執行,代碼解釋如下:
function *gen () {
yield 1
yield 2
return 3
}
const g = gen() // Iterator對象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
結合上述代碼和第一節的Generator 函數的特征可知:
在調用 gen 函數時,返回的一個 Iterator 對象,要想獲得每個 yield 表達式的狀態,需要調用 next 方法。
每次調用 next 方法時,都會返回一個包含 value 屬性和 done 屬性的對象,value 屬性表示 yield 表達式的值,done 屬性是一個布爾值,表示遍歷是否結束。
如果 value 沒有返回值或者 返回的是 undefined ,說明是函數運行結束了。
還有一種情況需要注意的額是:yield 表達式 如果用在另一個表達式中,需要為其加上圓括號“()”,作為函數參數和語句時可以不適用圓括號
function *gen () {
console.log('hello' + yield) ×
console.log('hello' + (yield)) √
console.log('hello' + yield '凱斯') ×
console.log('hello' + (yield '凱斯')) √
foo(yield 1) √
const param = yield 2 √
}
2、yield* 表達式
yield* 表達式的使用場景:在一個 Generator 函數中調用另一個 Generator 函數。下面代碼是不能執行的:
function *foo () {
yield 1
}
function *gen () {
foo()
yield 2
}
const g = gen()
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
如果使用 yield 表達式,value 返回的值是一個遍歷器對象:
function *foo () {
yield 1
}
function *gen () {
yield foo()
yield 2
}
const g = gen()
g.next() // {value: Generator, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
這里要實現上面場景,要使用 yield* 表達式,其實這個表達式就是 for...of 方法的簡寫:
function *foo () {
yield 1
}
function *gen () {
yield* foo()
yield 2
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
// 相當于
function *gen () {
yield 1
yield 2
}
// 相當于
function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}
yield* 遍歷具有 Iterator 接口的數據類型:
const arr = ['a', 'b']
const str = 'yuan'
function *gen () {
yield arr
yield* arr
yield str
yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'yuan', done: false}
g.next() // {value: 'y', done: false}
...
使用 yield* 表達式取出嵌套數組中的成員:
// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
console.log(+item) // 1, 2, 3, 4
}
// 使用yield*表達式
function *gen (arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield * gen(arr[i])
}
} else {
yield arr
}
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
console.log(item) // 1, 2, 3, 4
}
3、next 方法
next 方法的作用是分階段執行 Generator 函數。
每一次調用next方法,就會從函數頭部或者上一次停下來的地方開始執行,直到遇到下一個yield表達式(return 語句)為止。同時,調用next方法時,會返回包含value和done屬性的對象,value屬性值可以為yield表達式、return語句后面的值或者undefined值,done屬性表示遍歷是否結束。
遍歷器對象 next 方法的執行邏輯如下:
- 遇到yield表達式,就暫停執行后面的操作,并將緊跟在yield表達式后面的那個表達式的值,作為返回的對象的value屬性值。
- 下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
- 如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到遇到return語句為止,并將return語句后面表達式的值,作為返回的對象的value屬性值。
- 如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
function *gen () {
yield 1
yield 2
return 3
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
根據next運行邏輯再針對這個例子,就很容易理解了。調用gen函數,返回遍歷器對象。
第一次調用next方法時,在遇到第一個yield表達式時停止執行,value屬性值為1,即yield表達式后面的值,done為false表示遍歷沒有結束;
第二次調用next方法時,從暫停的yield表達式后開始執行,直到遇到下一個yield表達式后暫停執行,value屬性值為2,done為false;
第三次調用next方法時,從上一次暫停的yield表達式后開始執行,由于后面沒有yield表達式了,所以遇到return語句時函數執行結束,value屬性值為return語句后面的值,done屬性值為true表示已經遍歷完畢了。
第四次調用next方法時,value屬性值就是undefined了,此時done屬性為true表示遍歷完畢。以后再調用next方法都會是這兩個值。
4、next 方法的參數
yield 語句本身沒有返回值,或者說總是返回 undefined。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next() // {value: [undefined, NaN], done: true}
由上,第二次調用 next 方法時,并沒有返回相應的值,而是返回 undefined,解決方法:給第二次調用的 next 方法傳入一個參數,該參數會當作上一個 yield 語句的返回值。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next(10) // {value: [10, 5], done: true}```
注意,這里的 next 方法的參數是指上一個 yield 語句的返回值,所以在第一次調用 next 不能傳入參數。如若要傳參數,需要借用閉包來實現。
function wrapper (gen) {
return function (...args) {
const genObj = gen(...args)
genObj.next()
return genObj
}
}
const generator = wrapper(function *generator () {
console.log(`hello ${yield}`)
return 'done'
})
const a = generator().next('keith')
console.log(a) // hello keith, done
實際上,yield 表達式和 next 方法是 generator 函數的雙向信息傳遞。yield 表達式向外傳遞 value 值,next 方法的參數向內傳遞值。
5、遍歷 Generator 對象
與 Iterator 接口的關系
任何一個對象的Symbol.iterator屬性,指向默認的遍歷器對象生成函數。而Generator函數也是遍歷器對象生成函數,所以可以將Generator函數賦值給Symbol.iterator屬性,這樣就使對象具有了Iterator接口。默認情況下,對象是沒有Iterator接口的。
具有Iterator接口的對象,就可以被擴展運算符(...),解構賦值,Array.from和for...of循環遍歷了。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}
for...of 循環可以自動遍歷Generator函數生成的Iterator對象,不用調用next方法。
三、應用實例
1、協程
所謂協程:是指多個線程相互協作,完成異步任務。
Generator 函數是協程在 ES6 中的實現,最大特點就是可以交出函數的執行權。
整個Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方都用 yield 語句注明。Generator 函數實現協程代碼如下:
function* gen() {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next(); // { value: 3, done: false };
g.next(); // { value: undefined; done: true }
2、Thunk 函數
Thunk 函數是自動執行 Generator 函數的一種方法。
Thunk 函數的含義:起源于“傳名調用”的“求職策略”。將參數放到一個臨時函數之中,在將這個臨時參數傳入函數體。這個臨時函數就是 Thunk 函數。
在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數作為參數的單參數函數。
任何函數,只要參數有回調函數,都能寫成 Thenk 函數的形式。
Thunk 函數轉換器:
// ES5 版本
var Thunk = function(fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
args.push(callback);
return fn.apply(this, arg);
}
}
};
// ES6 版本
const Thunk = function(fn) {
return function(...args) {
return function(callback) {
return fn.call(this, ...args, callback);
}
}
}
使用上面的轉換器,生成, fs.redFile 的 Thunk 函數。
var readFileThunk = Thunk(fs.readFile);
redadFileThunk(fileA)(callback);`
Thunk 函數真正的威力在于可以自動執行 Generator 函數。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if(result.done) return;
result.value(next);
}
next();
}
function* g() {
...
}
run(g);
上面是一個基于 Thunk 函數的 Generator 執行器,可以在generator 函數內部執行多個異步操作。但是,這個里的每一個異步操作都必須是 Thunk 函數,也就是說,跟在 yield 表達式后面的必須是 Thunk 函數。
3、Co 模塊
co 模塊是用于 Generator 函數自動執行的一個小工具。
co 模塊是講兩種自動執行器(Thunk 函數和 Promise 對象)包裝成一個模塊。使用 co 模塊的前提條件是,Generator 函數的 yield 名利后面只是 Thunk 函數或 Promise 對象。
使用 co 模塊處理并發的異步操作 :
1、并發操作放在數組里
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
2、并發操作寫在對象里
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2)
};
console.log(res);
}).catch(onerror);
3、處理多個 generator 異步操作
co(function* () {
var valuse = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y;
}
上面代碼允許并發 3 個 somethingAsync 異步操作。
四、結語
本章需要了解的是生成器函數 Generator 的具體含義及作用,如何使用 Generator 函數,以及 Generator 函數的幾個特殊的應用實例。
章節目錄
1、ES6中啥是塊級作用域?運用在哪些地方?
2、ES6中使用解構賦值能帶給我們什么?
3、ES6字符串擴展增加了哪些?
4、ES6對正則做了哪些擴展?
5、ES6數值多了哪些擴展?
6、ES6函數擴展(箭頭函數)
7、ES6 數組給我們帶來哪些操作便利?
8、ES6 對象擴展
9、Symbol 數據類型在 ES6 中起什么作用?
10、Map 和 Set 兩數據結構在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、從 Promise 開始踏入異步操作之旅
13、ES6 迭代器(Iterator)和 for...of循環使用方法
14、ES6 異步進階第二步:Generator 函數
15、JavaScript 異步操作進階第三步:async 函數
16、ES6 構造函數語法糖:class 類