什麼是JavaScript generator 以及如何使用它們
在本文中,我們將要介紹 ECMAScript 6 中的 generator 是什麼,以及關於它們的使用案例。
什麼是 JavaScript generator
generators 是可以控制 iterator(迭代器)的函式。並在任何時候都可以暫停和恢復。
如果這不好理解,那讓我們看一些示例,這些示例將解釋 generator 是什麼,以及它和 iterator(迭代器,如 for-loop) 之間的區別。
這是一個立即輸出值的for 迴圈。這段程式碼是做什麼的?—— 只是輸出 0 到 5 之間的數
for (let i = 0; i < 5; i += 1) { console.log(i); } // 將會立即輸出 0 -> 1 -> 2 -> 3 -> 4 複製程式碼
現在讓我們看看 generator 函式:
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 迴圈。但是最主要的變化是,它不會立即執行。這是 generator 中最重要的特性 — 我們可以在真正需要下一個值的時候,才去獲取它,而不是一次獲得所有值。在有些情況下,這會非常方便。
generator 語法
如何宣告一個 generator 函式呢?我們有很多方法可以實現,但是最主要的是在函式關鍵字之後新增一個星號。
function * generator () {} function* generator () {} function *generator () {} let generator = function * () {} let generator = function* () {} let generator = function *() {} let generator = *() => {} // SyntaxError let generator = ()* => {} // SyntaxError let generator = (*) => {} // SyntaxError 複製程式碼
從上面的示例中可以看出,我們不能使用箭頭函式來建立一個 generator。
接下來 —— generator 作為一種方法。它的宣告方式與函式相同。
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只返回一次值,下次呼叫next() 時,它將執行到下一個yield 語句。
在 generator 中我們通常都會獲得一個物件作為輸出。它有兩個屬性value 和done 。正如你所想,value 表示返回的值,done 告訴我們 generator 是否完成了它的工作(是否迭代完成)。
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} - 繼續呼叫 next() 將返回相同的輸出。 複製程式碼
在 generator 中不僅可以使用yield ,return 語句也會返回相同的物件,但是當執行完第一個retrurn 語句時,迭代就會停止了。
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 delegator
帶星號的yield 可以代理執行另一個 generator。這樣你就可以根據需要連續呼叫多個 generator。
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 複製程式碼
在我們詳細介紹 generator 中的方法之前,讓我們來看看第一次看到會覺得很奇怪的一些行為。
下面這段程式碼向我們展示了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 ,它將返回我們傳遞的值。我們很快就會使用這個功能。
方法和初始化
generator 是可重用的,但是你需要初始化它們,幸運的是,這非常簡單。
function * generator(arg = 'Nothing') { yield arg; } const gen0 = generator(); // OK const gen1 = generator('Hello'); // OK const gen2 = new generator(); // Not OK generator().next(); // 這樣也可以,但是會每一次從都頭開始 複製程式碼
gen0和gen1 不會相互影響。gen2 不會起作用,甚至會報一個錯誤。合理的初始化一個generator 對於保持其內部的執行進度非常重要。
現在讓我們看看 generator 提供給我們的一些方法。
方法 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() 將返回相同的輸出。 複製程式碼
這將會是你最常使用的方法。每次呼叫它時,它都會為我們輸出一個物件。當所有的 yield 表示式被執行完之後,next() 會把屬性done 設定為true ,將屬性value 設定為undfined 。
我們不僅可以用next() 來迭代 generator,還可以用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 建構函式,因此我們需要另想辦法來新增自定義方法。 示例是我的辦法,當然你也可以選擇不同的方式。
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 複製程式碼
generator 的作用
在前面,我們用了已知迭代次數的 generator。但如果我們不知道要迭代多少次會怎麼樣呢?要想解決這個問題,在 generator 函式中建立一個死迴圈就足夠了。下面是一個返回隨機數的函式的例子:
function * randomFrom(...arr) { while (true) yield arr[Math.floor(Math.random() * arr.length)]; } const getRandom = randomFrom(1, 2, 5, 9, 4); getRandom.next().value; // 返回一個隨機數 複製程式碼
這很容易,但是對於更復雜的功能,例如節流函式。 如果你不知道它是什麼,請參考這篇文章。
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' 複製程式碼
還有沒有更好的使用 generator 的例子呢?如果你聽說過遞迴,我相信你也聽說過斐波那契數列。 通常它是通過遞迴來解決的,但是在 generator 的幫助下我們可以這樣寫它:
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} 複製程式碼
不再需要遞迴了!我們可以在需要的時候獲得數列中的下一個數字。
將 generator 用在 HTML 中
既然我們討論的是 JavaScript,那最顯著的方式就是使用 generator 對 HTML 進行一些操作。
因此,假設我們有一些 HTML 塊要處理,我們可以很容易地通過 generator 實現,但要記住,在不使用 generator 的情況下,還有很多可能的方法可以實現這一點。
ofollow,noindex">Using generator-iterator with HTML elements A PEN BY Vlad
我們只需要很少的程式碼就能完成此需求。
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 行邏輯程式碼。