一踢星、什么是生成器 Generator橘沥?
生成器對象是由一個 Generator 函數(shù)返回的,并且她符合 可迭代協(xié)議和迭代器協(xié)議桩撮。
語法:
function * gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"
生成器函數(shù)有以下四個特征:
- 在 function 關(guān)鍵字和函數(shù)名之間多了一個星號
- 函數(shù)內(nèi)部使用了 yield 表達(dá)式炮沐,用于定義 Generator 函數(shù)中的每個狀態(tài)
- Generator 函數(shù)通過多個 yield 表達(dá)式定義內(nèi)部狀態(tài)争群,掉用 Generator 函數(shù)時,不會像普通函數(shù)會立即執(zhí)行大年,而是返回的是一個 Iterator 對象换薄,通過調(diào)用 next() 方法,可以依次遍歷 Generator 函數(shù)的每個內(nèi)部狀態(tài)
- Generator 函數(shù)的星號的位置有四種情況 如下
function *gen () {}
function* gen () {}
function * gen () {}
function*gen () {}
一般的寫法是第二種鲜戒。
二专控、如何使用 Generator?
1遏餐、yield 表達(dá)式
yield 關(guān)鍵字在 Generator 函數(shù)中有兩個作用:定義內(nèi)部狀態(tài)和暫停執(zhí)行伦腐,代碼解釋如下:
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}
結(jié)合上述代碼和第一節(jié)的Generator 函數(shù)的特征可知:
在調(diào)用 gen 函數(shù)時,返回的一個 Iterator 對象失都,要想獲得每個 yield 表達(dá)式的狀態(tài)柏蘑,需要調(diào)用 next 方法。
每次調(diào)用 next 方法時粹庞,都會返回一個包含 value 屬性和 done 屬性的對象咳焚,value 屬性表示 yield 表達(dá)式的值,done 屬性是一個布爾值庞溜,表示遍歷是否結(jié)束革半。
如果 value 沒有返回值或者 返回的是 undefined ,說明是函數(shù)運行結(jié)束了流码。
還有一種情況需要注意的額是:yield 表達(dá)式 如果用在另一個表達(dá)式中又官,需要為其加上圓括號“()”,作為函數(shù)參數(shù)和語句時可以不適用圓括號
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* 表達(dá)式
yield* 表達(dá)式的使用場景:在一個 Generator 函數(shù)中調(diào)用另一個 Generator 函數(shù)六敬。下面代碼是不能執(zhí)行的:
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 表達(dá)式,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}
這里要實現(xiàn)上面場景驾荣,要使用 yield* 表達(dá)式外构,其實這個表達(dá)式就是 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}
// 相當(dāng)于
function *gen () {
yield 1
yield 2
}
// 相當(dāng)于
function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}
yield* 遍歷具有 Iterator 接口的數(shù)據(jù)類型:
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* 表達(dá)式取出嵌套數(shù)組中的成員:
// 普通方法
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*表達(dá)式
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 方法的作用是分階段執(zhí)行 Generator 函數(shù)播掷。
每一次調(diào)用next方法审编,就會從函數(shù)頭部或者上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達(dá)式(return 語句)為止歧匈。同時割笙,調(diào)用next方法時,會返回包含value和done屬性的對象,value屬性值可以為yield表達(dá)式伤溉、return語句后面的值或者undefined值,done屬性表示遍歷是否結(jié)束妻率。
遍歷器對象 next 方法的執(zhí)行邏輯如下:
- 遇到y(tǒng)ield表達(dá)式乱顾,就暫停執(zhí)行后面的操作,并將緊跟在yield表達(dá)式后面的那個表達(dá)式的值宫静,作為返回的對象的value屬性值走净。
- 下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行孤里,直到遇到下一個yield表達(dá)式。
- 如果沒有再遇到新的yield表達(dá)式,就一直運行到函數(shù)結(jié)束陵霉,直到遇到return語句為止貌夕,并將return語句后面表達(dá)式的值,作為返回的對象的value屬性值虏等。
- 如果該函數(shù)沒有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}
根據(jù)next運行邏輯再針對這個例子霍衫,就很容易理解了候引。調(diào)用gen函數(shù),返回遍歷器對象敦跌。
第一次調(diào)用next方法時澄干,在遇到第一個yield表達(dá)式時停止執(zhí)行,value屬性值為1柠傍,即yield表達(dá)式后面的值麸俘,done為false表示遍歷沒有結(jié)束;
第二次調(diào)用next方法時携兵,從暫停的yield表達(dá)式后開始執(zhí)行疾掰,直到遇到下一個yield表達(dá)式后暫停執(zhí)行,value屬性值為2徐紧,done為false静檬;
第三次調(diào)用next方法時,從上一次暫停的yield表達(dá)式后開始執(zhí)行并级,由于后面沒有yield表達(dá)式了拂檩,所以遇到return語句時函數(shù)執(zhí)行結(jié)束,value屬性值為return語句后面的值嘲碧,done屬性值為true表示已經(jīng)遍歷完畢了稻励。
第四次調(diào)用next方法時,value屬性值就是undefined了,此時done屬性為true表示遍歷完畢望抽。以后再調(diào)用next方法都會是這兩個值加矛。
4、next 方法的參數(shù)
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}
由上,第二次調(diào)用 next 方法時辑奈,并沒有返回相應(yīng)的值苛茂,而是返回 undefined,解決方法:給第二次調(diào)用的 next 方法傳入一個參數(shù)鸠窗,該參數(shù)會當(dāng)作上一個 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 方法的參數(shù)是指上一個 yield 語句的返回值稍计,所以在第一次調(diào)用 next 不能傳入?yún)?shù)躁绸。如若要傳參數(shù),需要借用閉包來實現(xiàn)丙猬。
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 表達(dá)式和 next 方法是 generator 函數(shù)的雙向信息傳遞。yield 表達(dá)式向外傳遞 value 值茧球,next 方法的參數(shù)向內(nèi)傳遞值庭瑰。
5、遍歷 Generator 對象
與 Iterator 接口的關(guān)系
任何一個對象的Symbol.iterator屬性抢埋,指向默認(rèn)的遍歷器對象生成函數(shù)弹灭。而Generator函數(shù)也是遍歷器對象生成函數(shù),所以可以將Generator函數(shù)賦值給Symbol.iterator屬性揪垄,這樣就使對象具有了Iterator接口穷吮。默認(rèn)情況下,對象是沒有Iterator接口的饥努。
具有Iterator接口的對象捡鱼,就可以被擴展運算符(...),解構(gòu)賦值酷愧,Array.from和for...of循環(huán)遍歷了驾诈。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}
for...of 循環(huán)可以自動遍歷Generator函數(shù)生成的Iterator對象,不用調(diào)用next方法溶浴。
三乍迄、應(yīng)用實例
1、協(xié)程
所謂協(xié)程:是指多個線程相互協(xié)作士败,完成異步任務(wù)闯两。
Generator 函數(shù)是協(xié)程在 ES6 中的實現(xiàn),最大特點就是可以交出函數(shù)的執(zhí)行權(quán)。
整個Generator 函數(shù)就是一個封裝的異步任務(wù)漾狼,或者說是異步任務(wù)的容器重慢。異步操作需要暫停的地方都用 yield 語句注明。Generator 函數(shù)實現(xiàn)協(xié)程代碼如下:
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 函數(shù)
Thunk 函數(shù)是自動執(zhí)行 Generator 函數(shù)的一種方法伤锚。
Thunk 函數(shù)的含義:起源于“傳名調(diào)用”的“求職策略”。將參數(shù)放到一個臨時函數(shù)之中志衣,在將這個臨時參數(shù)傳入函數(shù)體。這個臨時函數(shù)就是 Thunk 函數(shù)猛们。
在 JavaScript 語言中念脯,Thunk 函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù)弯淘,將其替換成一個只接受回調(diào)函數(shù)作為參數(shù)的單參數(shù)函數(shù)绿店。
任何函數(shù),只要參數(shù)有回調(diào)函數(shù)庐橙,都能寫成 Thenk 函數(shù)的形式假勿。
Thunk 函數(shù)轉(zhuǎn)換器:
// 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);
}
}
}
使用上面的轉(zhuǎn)換器,生成态鳖, fs.redFile 的 Thunk 函數(shù)转培。
var readFileThunk = Thunk(fs.readFile);
redadFileThunk(fileA)(callback);`
Thunk 函數(shù)真正的威力在于可以自動執(zhí)行 Generator 函數(shù)。
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 函數(shù)的 Generator 執(zhí)行器浆竭,可以在generator 函數(shù)內(nèi)部執(zhí)行多個異步操作浸须。但是,這個里的每一個異步操作都必須是 Thunk 函數(shù)邦泄,也就是說删窒,跟在 yield 表達(dá)式后面的必須是 Thunk 函數(shù)。
3顺囊、Co 模塊
co 模塊是用于 Generator 函數(shù)自動執(zhí)行的一個小工具肌索。
co 模塊是講兩種自動執(zhí)行器(Thunk 函數(shù)和 Promise 對象)包裝成一個模塊。使用 co 模塊的前提條件是特碳,Generator 函數(shù)的 yield 名利后面只是 Thunk 函數(shù)或 Promise 對象诚亚。
使用 co 模塊處理并發(fā)的異步操作 :
1、并發(fā)操作放在數(shù)組里
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
2测萎、并發(fā)操作寫在對象里
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;
}
上面代碼允許并發(fā) 3 個 somethingAsync 異步操作。
四硅瞧、結(jié)語
本章需要了解的是生成器函數(shù) Generator 的具體含義及作用份乒,如何使用 Generator 函數(shù),以及 Generator 函數(shù)的幾個特殊的應(yīng)用實例。
章節(jié)目錄
1或辖、ES6中啥是塊級作用域瘾英?運用在哪些地方?
2颂暇、ES6中使用解構(gòu)賦值能帶給我們什么缺谴?
3、ES6字符串?dāng)U展增加了哪些耳鸯?
4湿蛔、ES6對正則做了哪些擴展?
5县爬、ES6數(shù)值多了哪些擴展阳啥?
6、ES6函數(shù)擴展(箭頭函數(shù))
7财喳、ES6 數(shù)組給我們帶來哪些操作便利察迟?
8、ES6 對象擴展
9耳高、Symbol 數(shù)據(jù)類型在 ES6 中起什么作用扎瓶?
10、Map 和 Set 兩數(shù)據(jù)結(jié)構(gòu)在ES6的作用
11泌枪、ES6 中的Proxy 和 Reflect 到底是什么鬼概荷?
12、從 Promise 開始踏入異步操作之旅
13工闺、ES6 迭代器(Iterator)和 for...of循環(huán)使用方法
14乍赫、ES6 異步進(jìn)階第二步:Generator 函數(shù)
15、JavaScript 異步操作進(jìn)階第三步:async 函數(shù)
16陆蟆、ES6 構(gòu)造函數(shù)語法糖:class 類