
📢 大家好,我是小丞同學,本文將會帶你理解
ES6中的生成器,
寫在前面
在上篇文章中,我們深入了理解了迭代器的原理和作用,這一篇我們來深扒與迭代器息息相關的生成器,
關于生成器有這樣的描述
紅寶書:生成器是 ES6 新增的一個極為靈活的結構,擁有在一個函式塊內暫停和恢復代碼執行的能力
阮一峰老師:Generator 函式是 ES6 提供的一種異步編程解決方案
從上面的兩段話中,我們可以知道生成器有著至少兩個作用:
- 打破完整運行,擁有暫停和啟動的能力
- 解決異步操作
下面我們來看看生成器是如何實作這些功能的
一個例子了解生成器
我們先來看一個例子
下面是一個 for 回圈的例子,會在每次回圈中輸出當前的 index ,這段代碼很也是簡單的生成了 0-5 這些數字
for (let i = 0; i <= 5; i++) {
console.log(i);
}
// 輸出 0 1 2 3 4 5
我們再來看看利用生成器函式是怎么實作的
function* generatorForLoop(num) {
for (let i = 0; i <= num; i ++) {
yield console.log(i);
}
}
const gen = generatorForLoop(5);
gen.next(); // 0
gen.next(); // 1
gen.next(); // 2
gen.next(); // 3
gen.next(); // 4
gen.next(); // 5
我們可以看到,只有呼叫 next 方法,才會向下執行,而不會一次產生所有值,這就是一個最簡單的生成器了,在某些場景下,這種特性就成為了它的殺手锏
基本概念
1. 函式宣告
生成器的形式是一個函式,函式名稱前面加一個星號 * 表示它是一個生成器,
// 函式宣告
function * generator () {}
// 函式運算式
let generator = function *() {}
在定義一個生成器時,星號的位置在函式名前,但是位置沒有明確的要求,不需要考慮挨著誰,都可以
只要是可以定義函式的地方,就可以定義生成器,
需要特別注意的是:箭頭函式不能用來定義生成器
2. yield 運算式
函式體內部使用yield運算式,定義不同的內部狀態,我們來看一段代碼
function* helloWorld() {
yield 'hello';
yield 'world';
return 'ending';
}
在上面的代碼中定義了一個生成器函式 helloWorld ,內部有兩個 yield 運算式,三個狀態:hello,world 和 return 陳述句
作為生成器的核心,單純這么解釋可能還是不能明白 yield 的作用以及它的使用方法
下面我們來展開說說 yield 關鍵字
首先它和 return 關鍵字有些許的類似,return 陳述句會在完成函式呼叫后回傳值,但是在 return 陳述句之后無法進行任何操作

可以看到在編譯器中第一個 return 陳述句之后的代碼變灰了,說明了沒有生效,但是yield的作業方式卻不同,我們再來看看 yield 是如何作業的

注意:yield 關鍵字只能在生成器函式內部使用,其他地方使用會拋出錯誤
首先生成器函式會回傳一個遍歷器物件,只有通過呼叫 next 方法才會遍歷下一個狀態,而 yield 就是一個暫停的標志
在上面的代碼中,首先宣告了一個生成器函式,利用 myR 變數接收生成器函式的回傳值,也就是上面所說的遍歷器物件,此時遍歷器物件處于暫停狀態,
當呼叫 next 方法時,開始執行,遇到 yield 運算式,就暫停后面的操作,將 yield 后面的運算式的值,作為回傳的物件的 value 值,因此第一個 myR.next() 中的 value 值為 8
再次呼叫 next 方法時,再繼續向下執行,遇到 yield 再停止,后續操作一致
需要注意的是,yield 運算式后面的運算式,只有當呼叫next方法,內部指標指向該陳述句時才會執行
function* gen() {
yield 123 + 456;
}
就例如上面的代碼中,yield后面的運算式 123 + 456 ,不會立即求值,只會在 next 方法將指標移到這一句時,才會求值,
因此可以理解為 return 是結束, yield 是停止
3. 一定需要 yield 陳述句嗎?
其實在生成器函式中也可以沒有yield運算式,但是生成器的特性還在,那么它就變成了一個單純的暫緩執行函式,只有在呼叫該函式的遍歷器物件的 next 方法才會執行
function* hello() {
console.log('現在執行');
}
// 生成遍歷器物件
let generator = hello()
setTimeout(() => {
// 開始執行
generator.next()
}, 2000)
4. 注意
yield 運算式如果用在另一個運算式中,必須放在圓括號里
console.log('Hello' + (yield 123)); // OK
yield 運算式用作函式引數可以不加括號
foo(yield 'a')
如何理解 Generator 函式是狀態機?
在阮一峰老師的 ES6 書籍上有著對生成器函式這樣的理解
Generator函式有多種理解角度,語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態,
書上說,Generator 函式是狀態機,這是什么意思呢,狀態機又怎么理解呢?
這個和 JavaScript 的狀態模式有些許關聯
狀態模式:當一個物件的內部狀態發生改變時,會導致其行為的改變,這看起來像是改變了物件
看到這些定義的時候,顯然每個字都知道是什么意思,合起來卻不知所云
先不要慌,我們先來看看狀態模式是個什么東西,寫個狀態機就明白了
我們用一個洗衣機的例子,按一下電源鍵就打開,再按一下就關閉,我們先來實作這個
let switches = (function () {
let state = "off";
return function () {
if (state === "off") {
console.log("打開洗衣機");
state = "on";
} else if (state === "on") {
console.log("關閉洗衣機");
state = "off";
}
}
})();
在上面的代碼中,通過一個立即執行函式,回傳一個函式,將狀態 state 保存在函式內部,每次按下電源鍵呼叫 switches 函式即可,
這樣看起來很完美,下面我們改變一下需求,洗衣機上有一個調整模式的按鈕,每按一下換一個模式,假設有快速、洗滌、漂洗、拖水怎么實作
同樣的我們還是可以采用 if-else 陳述句實作
let switches = (function () {
let state = "快速";
return function () {
if (state === "快速") {
console.log("洗滌模式");
state = "洗滌";
} else if (state === "洗滌") {
console.log("漂洗模式");
state = "漂洗";
} else if (state === "漂洗") {
console.log("脫水模式");
state = "脫水";
} else if (state === "脫水") {
console.log("快速模式");
state = "快速";
}
}
})();
越來越復雜了,當模式再增多時,if-else 陳述句會越來越多,代碼會難以閱讀,你可能會說可以采用 switch-case 陳述句來實作,當然也可以,但是治標不治本,我們可不可以不采用判斷陳述句實作呢,回到我們剛開始的定義
狀態模式:當一個物件的內部狀態發生改變時,會導致其行為的改變,這看起來像是改變了物件
咦,想想,洗衣機不正是需要實作狀態改變,行為改變嗎?那這正可以采用狀態模式來實作呀,這里我們就直接引出我們的 generator 函式,通過控制狀態來改變它的行為
利用原型來實作的方法太過于復雜和冗余了,就不展示了
const fast = function () {
console.log("快速模式");
}
const wash = function () {
console.log("洗滌模式");
}
const rinse = function () {
console.log("漂洗模式");
}
const dehydration = function () {
console.log("脫水模式");
}
function* models() {
let i = 0,
fn, len = arguments.length;
while (true) {
fn = arguments[i++]
yield fn()
if (i === len) {
i = 0;
}
}
}
const exe = models(fast, wash, rinse, dehydration); //按照模式順序排放
在上面的代碼中我們只需要在每次按下時呼叫 next 方法即可切換下一個狀態

說了這么多 generator 為什么說是狀態機呢?我的理解是:當呼叫 Generator 函式獲取一個迭代器時,狀態機處于初態,迭代器呼叫 next 方法后,向下一個狀態跳轉,然后執行該狀態的代碼,當遇到 return 或最后一個 yield 時,進入終態,同時采用 Generator 實作的狀態機是最佳的結構,
next 傳遞引數
生成器的另一強大之處在于內建訊息輸入輸出能力,而這一能力仰仗于 yield 和 next 方法
yield 運算式本身沒有回傳值,或者說總是返回 undefined , next 方法可以帶一個引數,該引數就會被當作上一個 yield 運算式的回傳值,
從語意上講,第一個 next 方法用來啟動遍歷器物件,所以不用帶有引數,
來看一個例子
function* foo(x) {
let y = x * (yield)
return y
}
const it = foo(6)
it.next()
let res = it.next(7)
console.log(res.value) // 42
在上面的代碼中,呼叫 foo 函式回傳一個遍歷器物件 it ,并將 6 作為引數傳遞給 x ,呼叫遍歷器物件的 next 方法,啟動遍歷器物件,并且運行到第一個 yield 位置停止,
再次呼叫 next 方法傳入引數 7 ,作為上一個 yield 運算式的回傳值也就是 x 的乘項 (yield) 的值,運行到下一個 yield 或 return 結束
下面開始作死
在上面的例子中,如果不傳遞引數會這么樣呢?
在第二次運行 next 方法的時候不帶引數,導致了 y 的值等于 6 * undefined 也就是 NaN 所以回傳的物件的 value 屬性也是 NaN

我們再變一下
在原先的例子中,我們說第一個 next 是用來啟動遍歷器物件,那么如果傳入引數會怎么樣?
其實這樣傳遞引數是無效的,因為我們說 next 方法的引數表示上一個 yield 運算式的回傳值,
V8 引擎直接忽略第一次使用
next方法時的引數
與 Iterator 介面的關系
在上一篇中我們知道,一個物件的 Symbol.iterator 方法,等于該物件的遍歷器生成函式,呼叫該函式會回傳一個遍歷器物件
在這一篇我們知道,生成器函式就是遍歷器生成函式,那么是不是有什么想法了呢?
我們可以把生成器賦值給物件的 Symbol.iterator 屬性,實作 iterator 介面
let myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
}
[...myIterable] // [1, 2, 3]
提前終止生成器
生成器函式回傳的遍歷器物件,都有 next 方法,以及可選的 return 方法和 throw 方法
我們先來看 return 方法
return
return 方法會強制生成器進入關閉狀態,提供給 return 方法的值,就是終止迭代器物件的值,也就是說此時回傳的物件狀態為true,值為傳入的值,我們來驗證一下
function* genFn() {
for (const x of [1, 2, 3]) {
yield x
}
}
// 創建遍歷器物件 g
const g = genFn()
// 手動結束
console.log(g.return('結束'))
在上面的代碼中,輸出了 {value: "結束", done: true} ,這和我們預料的一樣,我們生成了遍歷器物件后,直接呼叫 return 終止了生成器
如果生成器函式內部有 try...finally 代碼塊,且正在執行 try 代碼塊,那么 return() 方法會導致立刻進入 finally 代碼塊,執行完以后,整個函式才會結束,
function* genFn() {
try {
yield 111
} finally {
console.log('我在finally中');
yield 999
}
}
// 創建遍歷器物件 g
const g = genFn()
// 啟動
g.next()
console.log(g.return('結束'))

在上面的代碼中,執行 next 函式,使得 try 代碼塊開始執行,再呼叫 return 方法,就會開始執行 finally 代碼塊,然后等待執行完畢,再回傳 return 方法指定的回傳值
throw
throw() 方法會在暫停的時候將一個提供的錯誤注入到生成器物件中,如果錯誤未被處理,生成器就會關閉
在很多的資料中都說的很復雜,其實就很簡單:
有錯誤你就給我一個 catch 來處理掉,不然你就給我退出,就是這么霸道
function* gen(){
console.log("state1");
let state1 = yield "state1";
console.log("state2");
let state2 = yield "state2";
console.log("end");
}
let g = gen();
g.next();
g.throw();
在上面的代碼中,throw 方法提出的錯誤,沒有被處理,因此會被直接退出,因此上面的代碼只會輸出 state1 ,然后報錯
注意:可以給 throw 方法傳遞引數,用來解釋錯誤
g.throw(new Error('出錯了!'))
next()、throw()、return() 的共同點
到這里遍歷器物件的3個方法,已經都涉及過了,雖然他們的功能各不相同,或者說完全沒有關系,但是他們的本質確實在做同一件事,“采用陳述句替換 yield 運算式”
next 是將 yield 運算式替換成一個值
throw是將 yield 運算式替換成 throw 陳述句
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));
return 是將 yield 運算式替換成 return 陳述句
yield* 運算式
帶星號的 yield,可以增強yield的行為,使它能夠迭代一個可迭代物件,從而一次產出一個值,這也叫委托迭代,通過這樣的方式,能將多個生成器連接在一起,
function * anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function * generator(i) {
yield* anotherGenerator(i);
}
var gen = generator(1);
gen.next().value; // 2
gen.next().value; // 3
gen.next().value; // 4
幾個注意點:
- 任何資料結構只要有
Iterator介面,就可以被yield*遍歷, - 如果被代理的
Generator函式有return陳述句,那么就可以向代理它的Generator函式回傳資料,
使用 yield* 實作遞回演算法
實作遞回演算法,這也是 yield* 最有用的地方,此時生成器可以產生自身
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0 1 2
上面的代碼中,每個生成器首先會從新創建的生成器物件產出每個值,然后再產出一個整數,
參考資料
[譯] 什么是 JavaScript 生成器?如何使用生成器?
阮一峰老師 Generator 函式的語法
《JavaScript高級程式設計第四版》
上篇文章:【深扒】 JavaScript 中的迭代器
本文內容就到這里結束了,關于生成器的核心應用異步編碼模式以及回呼問題,將在下篇總結,
非常感謝您的閱讀,歡迎提出你的意見,有什么問題歡迎指出,謝謝!🎈
我的博客即將同步至騰訊云+社區,邀請大家一同入駐
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/293754.html
標籤:其他
上一篇:vue中呼叫百度地圖 獲取經緯度
下一篇:HTML 二階段考試
