Mastering ‘Closures’ in JavaScript: Never to look back again



This content originally appeared on DEV Community and was authored by Saurabh Raj

Introduction

Closures are one of the most distinctive and powerful features of JavaScript, often hailed as both elegant and sometimes confusing for beginners. They are essential for mastering JavaScript, especially because they enable important programming patterns such as data encapsulation, emulating private methods, and maintaining state in a functional programming style.

What is a Closure?

Closure is the ability of a function to remember the variables that are declared in its outer scope. Even after the outer function has returned, the inner function continues to have access to those variables.

In code:

function outerFunction() {
  let outerVariable = 'I am from outer scope!';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const closureInstance = outerFunction();
// outerFunction has finished execution, but...
closureInstance(); // logs 'I am from outer scope!'

Here, outerFunction returns innerFunction, and innerFunction still remembers the value of outerVariable, even though outerFunction has already finished running. This “remembering” of the environment from which it came is what we call a closure.

Why Are Closures Important?

1. Data Encapsulation
Closures let you create private state. In many object-oriented languages, you have keywords like private or protected. JavaScript doesn’t natively offer these access modifiers, but closures provide a pattern to emulate them.

Example:

function createCounter() {
  let count = 0; // Private variable

  return {
    increment: function() {
      count++;
      console.log('Count:', count);
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment(); // Output: Count: 1
counter.increment(); // Output: Count: 2
console.log('Current Count:', counter.getCount()); // Output: Current Count: 2

2. Factory Functions
They allow you to create function factories that generate specialized functions. For instance, you might have a function that sets up some local variables and returns an inner function that uses those variables in specialized ways.

Example:

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

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

console.log('Double of 5:', double(5)); // Output: Double of 5: 10
console.log('Triple of 5:', triple(5)); // Output: Triple of 5: 15

3. Currying and Partial Application
Closures enable the creation of new functions from existing functions with some arguments pre-filled.

Example:

function add(a) {
  return function(b) {
    return a + b;
  };
}

const addFive = add(5);
console.log('5 + 10 =', addFive(10)); // Output: 5 + 10 = 15
console.log('5 + 20 =', addFive(20)); // Output: 5 + 20 = 25

4. Memoization
By storing computed results in the closure, you can optimize expensive function calls.

Example:

function memoizedFibonacci() {
  const cache = {}; // Closure to store computed values

  function fib(n) {
    if (n in cache) {
      return cache[n];
    }
    if (n < 2) {
      return n;
    }
    const result = fib(n - 1) + fib(n - 2);
    cache[n] = result;
    return result;
  }

  return fib;
}

const fib = memoizedFibonacci();
console.log('Fibonacci of 10:', fib(10)); // Output: Fibonacci of 10: 55

Lexical Scope (Must know to understand closures)

Lexical scope (sometimes called static scope) describes how a parser resolves variable names when functions are nested. When you write JavaScript code, the lexical scope is determined by the arrangement (the textual position) of your code. Functions are "linked" to the scope in which they are defined, not the scope in which they are invoked.

In other words, where you define your function in the code determines the variables your function has access to. It’s called “lexical” because it deals with the textual layout of your code, rather than dynamic conditions of how your function might get called.

Example:

var a = 1;

function first() {
  var a = 2;
  second();
}

function second() {
  console.log(a); // Output (1)?
}

first();
console.log(a); // Output (2)?

Output:
1
1

Here, even though second() is called inside first function, its lexical scope is determined at compile time, so it has access to the global variable ‘a’ (which is 1), and not the one local ‘a’ inside first function.

How Closures Work

When a function is created, it stores a reference to its outer (enclosing) environment. This stored reference is the closure. Even if the outer function finishes execution, the inner function retains access to the outer function’s variables.

Example: Basic closure

function createCounter() {
  let count = 0; // 'count' is in the outer function's scope

  function increment() { // 'increment' forms a closure over 'count'
    count++;
    console.log(count);
  }

  return increment;
}

const counter = createCounter();
counter(); // Output: 1
counter(); // Output: 2

Advanced Closure Concepts

1. Closures in Loops
Be cautious using closures within loops:

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}
// Output after 1 sec: 3 3 3

Explanation:
The output is 0 0 0 because var is function scoped and in every loop, the same variable is updated. But wait, number a primitive. Shouldn’t it be passed as a value? If yes, why does the console.log refer the updated variable value? Think for a moment. Explanation given below.

Even though primitives themselves are immutable and passed by value when used as function arguments, closures don’t capture the value of the variable at the time of creation. Instead, they capture the variable binding (i.e., a reference to the location where the value is stored in the lexical environment).

How It Works
– Variable Binding vs. Value Copy
When a closure is created, it “remembers” the variable (its binding) from the surrounding scope. This means that it doesn’t copy the value at that moment; it keeps a reference to the variable itself. So, any change to that variable later on will be reflected when the closure accesses it.

– Primitive Values:
Although primitives (like numbers, strings, booleans) are immutable and typically passed by value, the closure mechanism doesn’t take a snapshot of the value—it simply keeps a link to the variable holding that value. When the variable changes, the closure sees the updated value.

Fix Using let or IIFE

// With let
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}
// Output after 1 sec: 0 1 2

// With IIFE
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 1000);
    })(i);
}
// Output after 1 sec: 0 1 2

2. Closures with Immediately Invoked Function Expressions (IIFE)
An IIFE runs as soon as it’s defined and is great for encapsulation:

const counter = (function() {
    let count = 0;
    return () => ++count;
})();

console.log(counter()); // Output: 1
console.log(counter()); // Output: 2

3. Module Pattern
Closures help implement modules:

onst module = (function() {
    const privateVar = 'I am private';

    return {
        publicMethod: function() {
            console.log(privateVar);
        }
    };
})();

module.publicMethod(); // Output: I am private

5 Difficult, Twisted Questions to Master Closures

Question-1:

function mystery() {
    let x = 10;
    return function() {
        console.log(x++);
    };
}
const func1 = mystery();
const func2 = mystery();
func1();
func1();
func2();

Output:

10
11
10

Question-2:

const funcs = [];
for (var i = 0; i < 3; i++) {
    funcs[i] = function() {
        return i;
    };
}
console.log(funcs[0]());
console.log(funcs[1]());
console.log(funcs[2]());

Output

3
3
3

Question-3:
What’s wrong here, and how do you fix it without using let?

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), i * 100);
}

Question-4:

const add = (function(x) {
    return function(y) {
        return x + y;
    };
})(5);

console.log(add(2));
console.log(add(3));

Question-5:
Why does the following code work differently when var is replaced with let?

function testClosures() {
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100);
    }
}

function testClosuresLet() {
    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100);
    }
}

// Compare the outputs

Conclusion

Closures may seem daunting at first, but they’re a powerful tool on your journey to mastering JavaScript and building a rewarding career. Remember, every expert once started with the basics—so keep practicing, exploring, and refining your skills.

And finally, thank you for reading this article on JavaScript closures. I hope it clarified how closures work and how you can apply them to enhance your code. That’s all for now; be sure to check out the rest of my Never to Look Back series on Javascript. Happy coding!


This content originally appeared on DEV Community and was authored by Saurabh Raj