什么是JavaScript generator 以及如何使用它们
栏目: JavaScript · 发布时间: 6年前
内容简介:在本文中,我们将要介绍 ECMAScript 6 中的 generator 是什么,以及关于它们的使用案例。generators 是可以控制 iterator(迭代器)的函数。并在任何时候都可以暂停和恢复。如果这不好理解,那让我们看一些示例,这些示例将解释 generator 是什么,以及它和 iterator(迭代器,如 for-loop) 之间的区别。
在本文中,我们将要介绍 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 的情况下,还有很多可能的方法可以实现这一点。
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 行逻辑代码。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- RecyclerView使用指南(一)—— 基本使用
- 如何使用Meteorjs使用URL参数
- 使用 defer 还是不使用 defer?
- 使用 Typescript 加强 Vuex 使用体验
- [译] 何时使用 Rust?何时使用 Go?
- UDP协议的正确使用场合(谨慎使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。