Understanding JavaScript Closures with Examples

Understanding JavaScript Closures with Examples

Understanding JavaScript Closures with Examples

undefined undefined

Cutcu H.

Cutcu H.

October 1, 2024

Understanding JavaScript Closures with Examples
undefined undefined

Cutcu H.

Cutcu H.

October 1, 2024

Closures are an essential concept in JavaScript, often seen as a source of confusion for many developers. However, once you grasp how closures work, you will discover a powerful tool for controlling scope and improving the structure of your code. In this article, we'll demystify closures and explain them with simple examples.

What Is a Closure?
What Is a Closure?

What Is a Closure?

A closure is a function that "remembers" the environment in which it was created, even after that environment no longer exists. In simpler terms, it allows a function to access variables from its outer (enclosing) function scope even after the outer function has finished executing.

Closures provide data encapsulation, allowing you to retain state between function executions.

Basic Example of a Closure
Basic Example of a Closure

Basic Example of a Closure

Let's start with a basic example to see closures in action:

"basicExample.ts"

function outerFunction(outerVariable) {
  return function innerFunction(innerVariable) {
    console.log(`Outer Variable: ${outerVariable}`);
    console.log(`Inner Variable: ${innerVariable}`);
  };
}

const newFunction = outerFunction('outside');
newFunction('inside');

Output:

Outer Variable: outside
Inner Variable: inside
Explanation:
Explanation:

Explanation:

  • outerFunction returns innerFunction.
  • The innerFunction has access to outerVariable from its outer scope, even though outerFunction has finished executing.
  • This is what makes closures powerful; the inner function "remembers" the variables in its outer scope.
A Practical Use Case: Function Factories
A Practical Use Case: Function Factories

A Practical Use Case: Function Factories

A practical use of closures is to create function factories, where you generate customized functions.

"Example:"

function multiplier(factor) {
  return function (number) {
    return number * factor;
  };
}

const double = multiplier(2);
const triple = multiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Here, multiplier(2) returns a function that multiplies a number by 2. Similarly, multiplier(3) returns a function that multiplies a number by 3. The closure allows the returned function to access the factor variable, even after the multiplier function has finished executing.

Closures and Loops
Closures and Loops

Closures and Loops

Closures can sometimes behave unexpectedly, especially when combined with loops. Consider the following common example:

for (var i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(`i: ${i}`);
  }, 1000);
}

Expected Output:

i: 1
i: 2
i: 3

Actual Output:

i: 4
i: 4
i: 4
Explanation:
Explanation:

Explanation:

Due to how closures work, the inner function (inside setTimeout) refers to the same i from the outer scope, and by the time the timeout is executed, the loop has already completed, so i is 4. This is a common "gotcha" with closures in loops.

Fixing the Loop Issue with Closures
Fixing the Loop Issue with Closures

Fixing the Loop Issue with Closures

We can fix this issue by creating a new scope for each iteration. One way to do that is by using let instead of var, as let is block-scoped:

for (let i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(`i: ${i}`);
  }, 1000);
}

Now, each iteration creates a new block scope, and i retains its value in each closure, giving us the expected output:

i: 1
i: 2
i: 3

Alternatively, we can use an IIFE (Immediately Invoked Function Expression) to create a new scope in each iteration:

for (var i = 1; i <= 3; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(`i: ${i}`);
    }, 1000);
  })(i);
}

In this version, the IIFE captures the value of i in each iteration, preserving it for the setTimeout function.

Why Are Closures Useful?
Why Are Closures Useful?

Why Are Closures Useful?

Closures are not just theoretical constructs; they provide real utility in JavaScript:

  1. Data Encapsulation: Closures allow you to hide variables from the global scope, a common technique used to create private variables.

    "example"

    function counter() {
      let count = 0;
      return function () {
        count++;
        return count;
      };
    }
    
    const increment = counter();
    console.log(increment()); // 1
    console.log(increment()); // 2
    

    Here, count is not accessible outside of the counter function, but the inner function can access and modify it.

  2. Function Factories: As we saw in the multiplier example, closures allow you to create flexible and reusable functions.

  3. Callbacks and Event Handlers: Closures are commonly used in callbacks, especially for event handlers or asynchronous operations like setTimeout.

Conclusion
Conclusion

Conclusion

Closures are a core concept in JavaScript that allow functions to "remember" their outer scope. They are incredibly useful for maintaining state, creating flexible functions, and ensuring data privacy. By understanding closures and their practical applications, you'll be able to write more efficient and powerful JavaScript code.

Mastering closures is crucial to leveling up as a JavaScript developer, so take the time to experiment and get comfortable with this powerful concept!