深入了解ES6 JavaScript中的generator和yi

2021-03-30  本文已影响0人  魂斗驴

在本文中,我们将看一下ECMAScript 6中引入的 generator。我们将了解它的含义,然后再看一些使用它们的示例。

什么是JavaScript generator?

generator是可用于控制迭代器的函数。它们可以被挂起,以后可以随时恢复。

如果那没有道理,那么让我们看一些示例,这些示例将解释什么是 generator,以及 generator和诸如for-loop之类的迭代器之间的区别是什么。

这是一个for循环,可立即返回一堆值。该代码的作用是什么?—只是重复从0到5的数字。

for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
// this will return immediately 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(); // first console.log - 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4

它有什么作用?实际上,它只是对上面示例的for循环进行了一些更改。但是最重要的变化是它不会立即响起。这是 generator中最重要的功能-只有在真正需要时才可以获取下一个值,而不是一次获取所有值。在某些情况下,它可能非常方便。

generator 语法

我们如何声明 generator函数?有很多方法可以做到这一点,但是最主要的是在function关键字之后添加一个星号。


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; // we will never re-assign b
  return a * b; // and will never return new value
}

withReturn(6); // 11
withReturn(6); // 11

Yield 不同。


function * withYield(a) {
  let b = 5;
  yield a + b;
  b = 6; // it will be re-assigned after first execution
  yield a * b;
}

const calcSix = withYield(6);

calcSix.next().value; // 11
calcSix.next().value; // 36

Yield 仅返回一次值,下次您调用同一函数时,它将继续执行下一个yield 语句。

同样在 generator中,我们总是将对象作为输出。它始终具有两个属性valuedone。如您所料,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} - all other calls will produce the same result

不仅可以在 generator中使用yieldreturn还将向您返回相同的对象,但是在您到达第一个return语句之后, generator将完成其工作。

function * generator() {
  yield 1;
  return 2;
  yield 3; // we will never reach this yield
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}

Yield 代理

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

在继续介绍方法之前,让我们看一些第一次看起来很奇怪的行为。

这是正常的代码,没有任何错误,这表明我们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未定义的, 但是如果我们将传递任何值并仅调用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(); // It will work, but every time from the beginning

因此gen0gen1不会互相影响。并且gen2将不能工作,甚至更多,你会得到一个错误。初始化对于保持进度状态很重要。

现在,让我们看一下 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} and all next calls will return the same output

这是您最常使用的主要方法。每当我们调用它时,它都会为我们提供下一个输出对象。完成后,将next()done属性设置为truevalue设置undefined

我们不仅可以使用next()来迭代 generator。但是使用for-of循环,我们获得了 generator的所有值(而不是对象)。


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} - all next() calls after return() will return the same output

Return()将忽略 generator函数中的任何代码。但是将根据传递的参数设置值,并将完成设置为true。return()之后的所有next() 调用都将返回done-object。

方法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"}

// as Generator is not global variable we have to write something like this
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; // returns random 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} + 1s after -> '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}

无需递归更多!我们真正需要它们时可以得到下一个数字。

在HTML中使用 generator

由于我们在谈论JavaScript,最明显的使用 generator的方法是对HTML执行某些操作。

因此,假设我们要处理一些HTML块,我们可以使用 generator轻松实现这一点,但是请记住,没有 generator,还有许多可能的方法可以实现此目的。只需少量代码即可完成。


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);
});

实际上,只有五行逻辑。

总结

还有更多使用 generator的可能方法。例如,它们在使用异步操作时可能很有用。或遍历按需项目循环。

参考

What are JavaScript Generators and how to use them

上一篇 下一篇

猜你喜欢

热点阅读