DEV Community

Cover image for JavaScript Closures and Custom Iterators: A Comprehensive Guide.
TOMAS E GIL AMOEDO
TOMAS E GIL AMOEDO

Posted on

JavaScript Closures and Custom Iterators: A Comprehensive Guide.

Demystifying Closures in JavaScript

Closures are one of the most elegant and powerful features of JavaScript, allowing developers to write expressive and concise code. However, despite their potential, closures are often misunderstood and widely misinterpreted. In this blog, we will demystify closures, taking a deep dive into their inner workings and exploring how to create iterators using closures. By the end, you'll be equipped to harness the full power of closures in your own projects.

Let's start with the basics:

JavaScript and Lexical Scoping

JavaScript is a lexically scoped language, which fundamentally means that the data available when calling a function is determined by where it was originally defined. In other words, where the function was defined is what determines the data it has access to. If the available information varies depending on where it is called, we would be talking about a dynamic scope... but this is not the case.

Once this concept is understood, we can proceed with closures.

How closures work

Closures are formed from a function that returns a function, which is where functional programmers would be delighted with me. The inner function in this case is able to access variables and parameters from the outer function, even after the outer function has returned. This is possible because the inner function has formed a bond with its surrounding data, called closure or lexical closure.

In other words, the inner function has its own backpack, in which it carries around the data from its surrounding scope. When the inner function is returned, it retains access to that backpack, allowing it to access the same variables and parameters from the outer function whenever it is called.

To illustrate this concept, let's take a look at an example:

Two functions, one inside the other, in the inside function declaration the bond called closure gets solidified, and it's shown with something like a marker lines.

In this example, outerFunc defines a variable outerVar, and then returns the innerFunc. When innerFun is called, it logs the value of outerVar, even though it is no longer in the scope of outerFunc.

With closures, we can create powerful and flexible code. One example of this is the ability to create custom iterators, which allow us to iterate over data in a flexible and efficient way. With custom iterators, we can create complex operations on data with ease, giving JavaScript the full power of classic iteration libraries like Python's itertools.

But that's just the tip of the iceberg. With closures, you have infinite possibilities. You can create reusable functions that are tailored to your specific needs, or you can create encapsulated modules that protect your code from global namespace pollution. The only limit is your imagination.

In the next section, we'll explore how to create custom iterators using closures. Get ready to harness the full power of closures!

Let's take a look at a code example:


function createFlow(array) {
    let i = 0;
    function inner(){
        const element = array[i];
        i++;
        return element;
    }
    return inner;    
};

const returnNextElement = createFlow([4,5,6]);

const element1 = returnNextElement()
const element2 = returnNextElement()

Enter fullscreen mode Exit fullscreen mode

Now things get interesting because element1 is calling returnNextElement(), which has the following functionality within it:


function inner() {
    const element = array[i];
    i++;
    return element;
}

Enter fullscreen mode Exit fullscreen mode

But where is the array? We haven't defined it anywhere in the current execution context, so it should be undefined, right?

Here's where closures come in. As soon as we defined inner() inside createFlow(), it formed a bond with all the surrounding live memory, in other words, the surrounding data. When we returned that function, we returned in its backpack all of its surrounding data, which in this case happens to be the array [4,5,6], giving us access to its indexes.

So instead of looking for the array in the global memory, we look in the backpack of our returned function.

By the way, the posh name for returnNextElement is iterator. Iterators turn our collection data into streams, i.e., flows of actual values that we can access one after another.

With closures, we have the ability to create custom iterators, giving JavaScript the full power of classic iteration libraries like Python's itertools.

Creating Custom Iterators with Closures

In JavaScript, iterators are objects that provide a sequence of values from a collection. An iterator has a method called next which returns an object with two properties: value and done. The value property contains the next value in the sequence, and the done property is a boolean value that indicates whether the iterator has reached the end of the sequence.

With closures, we can easily create custom iterators that can iterate over any data type. Here's an example:

function returnIterator(arr) {
  let i = 0;
  function indIterator(){
    const element = arr[i];
    i ++;
    return element;
  }
  return indIterator;
}

const array = ['a', 'b', 'c', 'd'];
const myIterator = returnIterator(array);
console.log(myIterator()); // -> should log 'a'
console.log(myIterator()); // -> should log 'b'
console.log(myIterator()); // -> should log 'c'
console.log(myIterator()); // -> should log 'd'
Enter fullscreen mode Exit fullscreen mode

In this example, returnIterator is a function that takes an array and returns an iterator function. The iterator function indIterator has access to the arr variable in its closure and returns the next element of the array each time it is called.

We can also create an object with a next method to make our custom iterator more precise. Here's an example:

function nextIterator(arr) {
  let i = 0;
  const iterator = {
    next: function () {
      const value = arr[i];
      i++;
      return {
        value: value,
        done: i > arr.length
      };
    }
  };
  return iterator;
}

const array = ['a', 'b', 'c', 'd'];
const myIterator = nextIterator(array);
console.log(myIterator.next()); // -> should log { value: 'a', done: false }
console.log(myIterator.next()); // -> should log { value: 'b', done: false }
console.log(myIterator.next()); // -> should log { value: 'c', done: false }
console.log(myIterator.next()); // -> should log { value: 'd', done: false }
console.log(myIterator.next()); // -> should log { value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

In this example, nextIterator returns an object with a next method. Each time the next method is called, it returns an object with a value property and a done property. The value property is the next element in the array, and the done property is a boolean value that indicates whether the iterator has reached the end of the array.

Conclusion

Closures are a powerful feature in JavaScript that allow us to write more expressive and concise code. They enable us to create custom iterators, which are essential for many programming tasks. With closures, we can create reusable functions that are tailored to our specific needs, or we can create encapsulated modules that protect our code from global namespace pollution. The only limit is our imagination.

Top comments (0)