- 原文地址:What are JavaScript Generators and how to use them
- 原文做者:Vladislav Stepanov
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:lsvih
- 校对者:zhongdeming428,老教授
在本文中,咱们将了解 ECMAScript 6 中引入的生成器(Generator)。先看一看它到底是什么,而后用几个示例来讲明它的用法。javascript
生成器是一种能够用来控制迭代器(iterator)的函数,它能够随时暂停,并能够在任意时候恢复。前端
上面的描述无法说明什么,让咱们来看一些例子,解释什么是生成器,以及生成器与 for 循环之类的迭代器有什么区别。java
下面是一个 for 循环的例子,它会在执行后马上返回一些值。这段代码其实就是简单地生成了 0-5 这些数字。android
for (let i = 0; i < 5; i += 1) {
console.log(i);
}
// 它将会马上返回 0 -> 1 -> 2 -> 3 -> 4
复制代码
如今看看生成器函数。ios
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 循环作了一点改动,但产生了很大的变化。这种变化是由生成器最重要的特性形成的 —— 只有在须要的时候它才会产生下一个值,而不会一次性产生全部的值。在某些情景下,这种特性十分方便。git
如何定义一个生成器函数呢?下面列出了各类可行的定义方法,不过万变不离其宗的是在函数关键词后加上一个星号。github
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)来建立。定义方法与定义函数的方式是同样的。bash
class MyClass {
*generator() {}
* generator() {}
}
const obj = {
*generator() {}
* generator() {}
}
复制代码
如今让咱们一块儿看看新的关键词 yield
。它有些相似 return
,但又不彻底相同。return
会在完成函数调用后简单地将值返回,在 return
语句以后你没法进行任何操做。dom
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
能够将它的工做委托给另外一个生成器。经过这种方式,你就能将多个生成器链接在一块儿。
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}
复制代码
再也不须要递归了!咱们能够在须要的时候得到数列中的下一个数字。
既然是讨论 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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。