- 原文地址:What are JavaScript Generators and how to use them
- 原文作者:Vladislav Stepanov
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:lsvih
- 校對者:zhongdeming428,老教授
在本文中,我們將瞭解 ECMAScript 6 中引入的生成器(Generator)。先看一看它究竟是什麼,然後用幾個示例來說明它的用法。
什麼是 JavaScript 生成器?
生成器是一種可以用來控制迭代器(iterator)的函式,它可以隨時暫停,並可以在任意時候恢復。
上面的描述沒法說明什麼,讓我們來看一些例子,解釋什麼是生成器,以及生成器與 for 迴圈之類的迭代器有什麼區別。
下面是一個 for 迴圈的例子,它會在執行後立刻返回一些值。這段程式碼其實就是簡單地生成了 0-5 這些數字。
for (let i = 0; i < 5; i += 1) {
console.log(i);
}
// 它將會立刻返回 0 -> 1 -> 2 -> 3 -> 4
複製程式碼
現在看看生成器函式。
function * generatorForLoop(num) {
for (let i = 0; i < num; i += 1) {
yield console.log(i);
}
}
const genForLoop = generatorForLoop(5);
genForLoop.next(); // 首先 console.log - 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4
複製程式碼
它做了什麼?它實際上只是對上面例子中的 for 迴圈做了一點改動,但產生了很大的變化。這種變化是由生成器最重要的特性造成的 —— 只有在需要的時候它才會產生下一個值,而不會一次性產生所有的值。在某些情景下,這種特性十分方便。
生成器語法
如何定義一個生成器函式呢?下面列出了各種可行的定義方法,不過萬變不離其宗的是在函式關鍵詞後加上一個星號。
function * generator () {}
function* generator () {}
function *generator () {}
let generator = function * () {}
let generator = function* () {}
let generator = function *() {}
let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError
複製程式碼
如上面的例子所示,我們並不能使用箭頭函式來建立一個生成器。
下面將生成器作為方法(method)來建立。定義方法與定義函式的方式是一樣的。
class MyClass {
*generator() {}
* generator() {}
}
const obj = {
*generator() {}
* generator() {}
}
複製程式碼
yield
現在讓我們一起看看新的關鍵詞 yield
。它有些類似 return
,但又不完全相同。return
會在完成函式呼叫後簡單地將值返回,在 return
語句之後你無法進行任何操作。
function withReturn(a) {
let b = 5;
return a + b;
b = 6; // 不可能重新定義 b 了
return a * b; // 這兒新的值沒可能返回了
}
withReturn(6); // 11
withReturn(6); // 11
複製程式碼
而 yield
的工作方式卻不同。
function * withYield(a) {
let b = 5;
yield a + b;
b = 6; // 在第一次呼叫後仍可以重新定義變數
yield a * b;
}
const calcSix = withYield(6);
calcSix.next().value; // 11
calcSix.next().value; // 36
複製程式碼
用 yield
返回的值只會返回一次,當你再次呼叫同一個函式的時候,它會執行至下一個 yield
語句處(譯者注:前面的 yield
不再返回東西了)。
在生成器中,我們通常會在輸出時得到一個物件。這個物件有兩個屬性:value
與 done
。如你所想,value
為返回值,done
則會顯示生成器是否完成了它的工作。
function * generator() {
yield 5;
}
const gen = generator();
gen.next(); // {value: 5, done: false}
gen.next(); // {value: undefined, done: true}
gen.next(); // {value: undefined, done: true} - 之後的任何呼叫都會返回相同的結果
複製程式碼
在生成器中,不僅可以使用 yield
,也可以使用 return
來返回同樣的物件。但是,在函式執行到第一個 return
語句的時候,生成器將結束它的工作。
function * generator() {
yield 1;
return 2;
yield 3; // 到不了這個 yield 了
}
const gen = generator();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}
複製程式碼
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
複製程式碼
在開始下一節前,我們先觀察一個第一眼看上去比較奇特的行為。
下面是正常的程式碼,不會報出任何錯誤,這表明 yield
可以在 next()
方法呼叫後返回傳遞的值:
function * generator(arr) {
for (const i in arr) {
yield i;
yield yield;
yield(yield);
}
}
const gen = generator([0,1]);
gen.next(); // {value: "0", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A", done: false}
gen.next(); // {value: "1", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B", done: false}
gen.next(); // {value: undefined, done: true}
複製程式碼
在這個例子中,你可以看到 yield
預設是 undefined
,但如果我們在呼叫 yield
時傳遞了任何值,它就會返回我們傳入的值。我們將很快利用這個特性。
初始化與方法
生成器是可以被複用的,但是你需要對它們進行初始化。還好初始化的方法十分簡單。
function * generator(arg = 'Nothing') {
yield arg;
}
const gen0 = generator(); // OK
const gen1 = generator('Hello'); // OK
const gen2 = new generator(); // 不 OK
generator().next(); // 可以執行,但每次都會從頭開始執行
複製程式碼
如上所示,gen0
與 gen1
不會互相影響,gen2
完全不會執行(會報錯)。因此初始化對於保證程式流程的狀態是十分重要的。
下面讓我們一起看看生成器給我們提供的方法。
next() 方法
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: false}
gen.next(); // {value: undefined, done: true} 之後所有的 next 呼叫都會返回同樣的輸出
複製程式碼
這是最常用的方法。它每次被呼叫時都會返回下一個物件。在生成器工作結束時,next()
會將 done
屬性設為 true
,value
屬性設為 undefined
。
我們不僅可以用 next()
來迭代生成器,還可以用 for of
迴圈來一次得到生成器所有的值(而不是物件)。
function * generator(arr) {
for (const el in arr)
yield el;
}
const gen = generator([0, 1, 2]);
for (const g of gen) {
console.log(g); // 0 -> 1 -> 2
}
gen.next(); // {value: undefined, done: true}
複製程式碼
但它不適用於 for in
迴圈,並且不能直接用數字下標來訪問屬性:generator[0] = undefined
。
return() 方法
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.return(); // {value: undefined, done: true}
gen.return('Heeyyaa'); // {value: "Heeyyaa", done: true}
gen.next(); // {value: undefined, done: true} - 在 return() 之後的所有 next() 呼叫都會返回相同的輸出
複製程式碼
return()
將會忽略生成器中的任何程式碼。它會根據傳值設定 value
,並將 done
設為 true
。任何在 return()
之後進行的 next()
呼叫都會返回 done
屬性為 true
的物件。
throw() 方法
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.throw('Something bad'); // 會報錯 Error Uncaught Something bad
gen.next(); // {value: undefined, done: true}
複製程式碼
throw()
做的事非常簡單 —— 就是丟擲錯誤。我們可以用 try-catch
來處理。
自定義方法的實現
由於我們無法直接訪問 Generator
的 constructor,因此如何增加新的方法需要另外說明。下面是我的方法,你也可以用不同的方式實現:
function * generator() {
yield 1;
}
generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}
// 由於 Generator 不是一個全域性變數,因此我們只能這麼寫:
generator.prototype.__proto__.math = function(e = 0) {
return e * Math.PI;
}
generator.prototype.__proto__; // Generator {math: ƒ, constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, …}
const gen = generator();
gen.math(1); // 3.141592653589793
複製程式碼
生成器的用途
在前面,我們用了已知迭代次數的生成器。但如果我們不知道要迭代多少次會怎麼樣呢?為了解決這個問題,需要在生成器函式中建立一個無限迴圈。下面以一個會返回隨機數的函式為例進行演示:
function * randomFrom(...arr) {
while (true)
yield arr[Math.floor(Math.random() * arr.length)];
}
const getRandom = randomFrom(1, 2, 5, 9, 4);
getRandom.next().value; // 返回隨機的一個數
複製程式碼
這是個簡單的例子。下面來舉一些更復雜的函式為例,我們要寫一個節流(throttle)函式。如果你還不知道節流函式是什麼,請參閱這篇文章。
function * throttle(func, time) {
let timerID = null;
function throttled(arg) {
clearTimeout(timerID);
timerID = setTimeout(func.bind(window, arg), time);
}
while (true)
throttled(yield);
}
const thr = throttle(console.log, 1000);
thr.next(); // {value: undefined, done: false}
thr.next('hello'); // 返回 {value: undefined, done: false} ,然後 1 秒後輸出 'hello'
複製程式碼
還有沒有更好的利用生成器的例子呢?如果你瞭解遞迴,那你肯定聽過斐波那契數列。通常我們是用遞迴來解決這個問題的,但有了生成器後,可以這樣寫:
function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
return seed2;
})();
}
}
const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}
複製程式碼
不再需要遞迴了!我們可以在需要的時候獲得數列中的下一個數字。
將生成器用在 HTML 上
既然是討論 JavaScript,那顯然要用生成器來操作下 HTML。
假設有一些 HTML 塊需要處理,可以使用生成器來輕鬆實現。(當然除了生成器之外還有很多方法可以做到)
我們只需要少許程式碼就能完成此需求。
const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';
function * addClassToEach(elements, className) {
for (const el of Array.from(elements))
yield el.classList.add(className);
}
const addClassToStrings = addClassToEach(strings, className);
btn.addEventListener('click', (el) => {
if (addClassToStrings.next().done)
el.target.classList.add(className);
});
複製程式碼
僅有 5 行邏輯程式碼。
總結
還有更多使用生成器的方法。例如,在進行非同步操作或者按需迴圈時生成器也非常有用。
我希望這篇文章能幫你更好地理解 JavaScript 生成器。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。