by Ashay Mandwarya 🖋️💻🍕

Yield! Yield! How Generators work in JavaScript.

Photo by Frederik Trovatten.com on Unsplash

If the title doesn’t already give a hint, we will be discussing generators in this piece.

Before going into generators let’s revise some basics about functions.

  • In JavaScript, functions are a set of statements that perform a task and return some value ending the function.
  • If you call a function, again and again, it will execute all the statements again and again.
  • Arrows once left from the bow cannot be stopped — they only either hit or miss. The same way a function once called cannot be stopped, it will run, return a value, throw an error and then will stop after executing all the statements.

We only need to keep in mind these 3 points to understand generators.

Generators

A generator is a special type of function which can stop its execution midway and then start from the same point after some time. Generators are a combination of functions and iterators. This is a bit of a confusing statement but I will make sure by the end of the article this line will be clear.

For clarity, consider playing a game and suddenly mom calls for some work. You pause the game, help her, then resume playing again. It is the same with generators.

An iterator is an object which defines a sequence and potentially a return value upon its termination. — MDN.

Iterators in themselves are a huge topic and are not the aim of this article.

Basic Syntax

Generators are defined as a function with an asterisk(*) beside the function.

function* name(arguments) {   statements}

name — The function name.

arguments — Arguments for the function.

statements — The body of the function.

Return

A function can return almost anything ranging from a value, object or another function itself. A generator function returns a special object called the generator object (not entirely true). The object looks like the snippet below

{   value: value,  done: true|false}

The object has two properties value and done . The value contains the value to be yielded. Done consists of a Boolean (true|false) which tells the generator if .next() will yield a value or undefined.

The above statement will be difficult to digest. Let's change that with an example.

function* generator(e) {  yield e + 10;  yield e + 25;  yield e + 33;}var generate = generator(27);
console.log(generate.next().value); // 37console.log(generate.next().value); // 52console.log(generate.next().value); // 60console.log(generate.next().value); // undefined

Let’s understand the mechanics of the above code line by line.

lines 1–5: Lines 1–5 define the generator having the same name with an argument e. Inside the body of the function, it contains a bunch of statements with the keyword yield and some operation is done after that.

line 6: Line 6 assigns the generator to a variable called generate.

lines 8–11: These lines call a bunch of console.log each calling the generator chained to a next method which calls for the value property of the generator object.

Whenever a generator function is called upon, unlike normal functions it does not start execution right away. Instead, an iterator is returned (the actual reason * is used by a generator. It tells JS that an iterator object is to be returned). When the next()method of the iterator is called, the execution of the generator starts and executes until it finds the first yield statement. At this yield point the generator object is returned, the specifications of which are already explained. Calling the next() function again will resume the generator function until it finds another yield statement and the cycle returns till all yields are exhausted.

After this point if next is called it returns the generator object with value undefined.

Now let’s try yielding another generator function from the original generator and also a return statement.

A return statement in a generator will make the generator finish its execution like every other function. The done property of the generator object will be set to true and the value returned will be set to the value property of the generator object. All other yields will return undefined.

If an error is thrown then also the execution of the generator will stop, yielding a generator itself.

For yielding a generator we need to specify an * against the yield so as to tell JS that a generator is yielded. The yield* delegates to another generator function — that is the reason we can yield all the values of the generator2 function using the generate.next() of the original generator function. The first value yielded is from the first generator function and the last two yielded values are generated by the generator function but yielded by the original generator.

Advantages

Lazy loading

Lazy loading is essentially value evaluation only when there is a need for it. As we will see in a coming example, we can actually do it with generators. We might only yield the values when needed and not all at the same time.

The below example is from another example in this article and it generates infinite random numbers. Here we can see we can call as many next() as we want and not get all the values it is producing. Only the needed ones.

function * randomize() {  while (true) {let random = Math.floor(Math.random()*1000);    yield random;  }}
var random= randomize();
console.log(random.next().value)

Memory Efficient

As we can deduce from the above example, generators are extremely memory efficient. As we want the values only according to need, we need very less storage for storing those values.

Pitfalls

Generators are extremely useful but also have their own pitfalls too.

  • Generators don’t provide random access like arrays and other data structures. As the values are yielded one by one on call we cannot access random elements.
  • Generators provide one-time access. Generators don’t allow you to iterate the values again and again. Once all the values are exhausted we have to make a new Generator instance to iterate all the values again.

Why do we need Generators?

Generators provide a wide variety of uses in JavaScript. Let’s try to recreate some ourselves.

Implementing Iterators

An iterator is an object that enables a programmer to traverse a container -Wikipedia

We will print all the words present in a string using iterators. Strings are iterators too.

Iterators

const string = 'abcde';const iterator = string[Symbol.iterator]();console.log(iterator.next().value)console.log(iterator.next().value)console.log(iterator.next().value)console.log(iterator.next().value)console.log(iterator.next().value)

Here’s the same thing using generators

function * iterator() {yield 'a';yield 'b';yield 'c';yield 'd';yield 'e';}for (let x of iterator()) {console.log(x);}

Comparing both the methods, it is easy to see that with the help of generators we can do it with less clutter. I know it is not a very good example but enough to prove the following points:

  • No implementation of next()
  • No [Symbol.iterator]() invocation
  • In some cases, we even need to set the object.done property return value to true/false using iterators.

Async-Await ~ Promises+Generators

You can read my previous article about Async/Await if you want to learn about them, and check out this for Promises.

Crudely, Async/Await is just an implementation of Generators used with Promises.

Async-Await

async function async-await(){let a=await(task1);console.log(a);
let b=await(task2);console.log(b);
let c=await(task3);console.log(c);
}

Promises+Generators

function * generator-promise(){let a=yield Promise1();console.log(a);let b=yield Promise1();console.log(b);let c=yield Promise1();console.log(c);
}

As we can see, both produce the same result and almost in a similar fashion too. It’s because the Async/Await mechanism is loosely based on a combination of generators and promise. There is a lot more to Async/Await than shown above, but just for showing the use of a generator, we can consider this.

Infinite Data Structure

The heading might be a little misleading, but it is true. We can create generators, with the use of a while loop that will never end and will always yield a value.

function * randomize() {  while (true) {let random = Math.floor(Math.random()*1000);    yield random;  }}var random= randomize();while(true)console.log(random.next().value)

In the above snippet, we create an infinite generator, which will yield a random number on every next() invocation. It can be called as an infinite stream of random numbers. This is a very basic example.

Conclusion

There is yet a lot to be covered about generators, and this was just an introduction to the topic. Hope you learned something new and the article was easy to understand.

Follow me and applaud!