在本文中,咱们将要介绍 ECMAScript 6 中的 generator 是什么,以及关于它们的使用案例。javascript
generators 是能够控制 iterator(迭代器)的函数。并在任什么时候候均可以暂停和恢复。前端
若是这很差理解,那让咱们看一些示例,这些示例将解释 generator 是什么,以及它和 iterator(迭代器,如 for-loop) 之间的区别。java
这是一个当即输出值的 for 循环。这段代码是作什么的?—— 只是输出 0 到 5 之间的数git
for (let i = 0; i < 5; i += 1) {
console.log(i);
}
// 将会当即输出 0 -> 1 -> 2 -> 3 -> 4
复制代码
如今让咱们看看 generator 函数:github
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 中最重要的特性 — 咱们能够在真正须要下一个值的时候,才去获取它,而不是一次得到全部值。在有些状况下,这会很是方便。dom
如何声明一个 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 做为一种方法。它的声明方式与函数相同。oop
class MyClass {
*generator() {}
* generator() {}
}
const obj = {
*generator() {}
* generator() {}
}
复制代码
如今,让咱们来看看新的关键字 yield。它有点像 return,但不是。return 只在函数调用以后返回值,return 语句以后不容许你执行任何其余操做。ui
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 能够代理执行另外一个 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 函数中建立一个死循环就足够了。下面是一个返回随机数的函数的例子:
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}
复制代码
再也不须要递归了!咱们能够在须要的时候得到数列中的下一个数字。
既然咱们讨论的是 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 行逻辑代码。
使用 generator 还有不少能作的事情。例如,在进行异步操做时,它能够颇有用。或者是遍历一个按需项循环。
我但愿这篇文章能帮助你更好地理解 JavaScript generator。