在本文中, 我们将了解 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) 函数. 如果你还不知道节流函数是什么, 请参阅这篇文章 https://medium.com/@_jh3y/throttling-and-debouncing-in-javascript-b01cad5c8edf .
- 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'
还有没有更好的利用生成器的例子呢? 如果你了解递归, 那你肯定听过斐波那契数列 https://en.wikipedia.org/wiki/Fibonacci_number . 通常我们是用递归来解决这个问题的, 但有了生成器后, 可以这样写:
- 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 生成器.
来源: https://juejin.im/post/5b14faf2f265da6e4d5af3b9