js yield keyword

In this article, we'll look at the Generator introduced in ECMAScript 6. Let's take a look at what it is, and then use a few examples to illustrate its usage.

What is JavaScript generator

A generator is a function that can be used to control an iterator. It can be paused at any time and resumed at any time.

The above description can't explain anything. Let's take a look at some examples to explain what a generator is and the difference between a generator and an iterator such as a for loop.

The following is an example of a for loop that returns some values immediately after execution. This code simply generates the numbers 0-5.

for (let i = 0; i < 5; i += 1) {
  console.log(i);
}
// It will immediately return 0 - > 1 - > 2 - > 3 - > 4

Now look generator function .

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

What did it do? It actually just makes a little change to the for loop in the above example, but it has changed a lot. This change is caused by the most important feature of the generator - it produces the next value only when needed, not all values at once. In some situations, this feature is very convenient.

Generator syntax

How to define a generator function? Various feasible definition methods are listed below, but the main thing is to add an asterisk after the function keyword.

function * generator () {}
function* generator () {}
function *generator () {}

let generator = function * () {}
let generator = function* () {}
let generator = function *() {}

let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError

As the above example shows, we cannot use Arrow function To create a generator.

Next, the generator is created as a method. Defining a method is the same as defining a function.

class MyClass {
  *generator() {}
  * generator() {}
}

const obj = {
  *generator() {}
  * generator() {}
}

yield

Now let's look at the new keyword {yield. It's a little like return, but it's not exactly the same. Return will simply return the value after the function call is completed. You can't do anything after the return statement.

function withReturn(a) {
  let b = 5;
  return a + b;
  b = 6; // It's impossible to redefine b
  return a * b; // The new value here can't be returned
}

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

The way yield works is different.

function * withYield(a) {
  let b = 5;
  yield a + b;
  b = 6; // Variables can still be redefined after the first call
  yield a * b;
}

const calcSix = withYield(6);

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

The value returned with "yield" will only be returned once. When you call the same function again, it will execute to the next "yield" statement.

In a generator, we usually get an object when we output it. This object has two properties: value , and , done. As you might expect, value is the return value, and done will show whether the generator has completed its work.

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} - any subsequent call will return the same result

In the generator, you can not only use "yield", but also use "return" to return the same object. However, when the function reaches the first return statement, the generator will end its work.

function * generator() {
  yield 1;
  return 2;
  yield 3; // I can't get to this yield
}

const gen = generator();

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

yield delegate iteration

An asterisk {yield} can delegate its work to another generator. In this way, you can connect multiple generators together.

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

Before we begin the next section, let's look at a behavior that looks strange at first sight.

The following is the normal code and no error will be reported, which indicates that {yield} can return the passed value after the} next() method call:

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}

In this example, you can see that "yield" is "undefined" by default, but if we pass any value when calling "yield", it will return the value we passed in. We will take advantage of this feature soon.

Initialization and method

Generators can be reused, but you need to initialize them. Fortunately, the initialization method is very simple.

function * generator(arg = 'Nothing') {
  yield arg;
}

const gen0 = generator(); // OK
const gen1 = generator('Hello'); // OK
const gen2 = new generator(); // Not OK

generator().next(); // It can run, but it will run from the beginning every time

As shown above, gen0 , and gen1 , will not affect each other, and gen2 , will not run at all (an error will be reported). Therefore, initialization is very important to ensure the state of the program process.

Let's take a look at the methods provided by the generator.

next() method

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(); // After {value: undefined, done: true}, all next calls will return the same output

This is the most commonly used method. It returns the next object each time it is called. At the end of the generator work, next() will set the "done" attribute to "true" and the "value" attribute to "undefined".

We can not only iterate over the generator with {next(), but also use the} for} loop to get all the values of the generator (not the object) at once.

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}

However, it is not applicable to the {for in} loop, and the attribute cannot be accessed directly with a numeric subscript: generator[0] = undefined.

return() method

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() will ignore any code in the generator. It will set "value" according to the passed value and "done" to "true". Any call to {next() after} return() will return an object whose} done} attribute is {true}.

throw() method

function * generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();

gen.throw('Something bad'); // Error Uncaught Something bad will be reported
gen.next(); // {value: undefined, done: true}

What throw() does is very simple -- it just throws an error. We can use , try catch , to handle it.

Implementation of custom methods

Since we can't directly access the constructor of the Generator, how to add a new method needs to be explained separately. Here is my method, which you can implement in different ways:

function * generator() {
  yield 1;
}

generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}

// Because the Generator is not a global variable,So we can only write:
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

Purpose of generator

Earlier, we used a generator with a known number of iterations. But what if we don't know how many iterations to iterate? To solve this problem, you need to create an infinite loop in the generator function. The following will return with a random number As an example:

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 a random number

This is a simple example. Let's take some more complex functions as examples. We're going to write a throttle function. If you don't know yet Throttling function What is it? See This article.

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'); // Return {value: undefined, done: false}, and then output 'hello' in 1 second

Are there any examples of better use of generators? If you know recursion, you must have heard of it Fibonacci sequence . We usually use recursion to solve this problem, but with a generator, we can write as follows:

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}

No more recursion! We can get the next number in the sequence when we need it.

Using generators on HTML

Since we are talking about JavaScript, we obviously need to use the generator to operate HTML.

Assuming that there are some HTML blocks that need to be processed, you can easily implement them using a generator. (of course, there are many ways to do this besides the generator)

We only need a little code to accomplish this requirement.

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

Only 5 lines Logic code.

Keywords: Javascript

Added by Kookedoh on Mon, 13 Dec 2021 04:49:53 +0200