DEV Community

Rahul Vijayvergiya
Rahul Vijayvergiya

Posted on

JavaScript Closures

JavaScript closures are a fundamental concept that every developer should understand. They can seem tricky at first, but once you grasp how they work, you'll find them to be a powerful feature of the language. In this article, we'll explore what closures are, how they work, and provide some practical examples to illustrate their use.

What is a Closure?

A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables, even after the outer function has finished executing. Closures are created every time a function is created, at function creation time.

In simpler terms, a closure is formed when a function retains access to its lexical scope, even when the function is executed outside that scope.

Lexical Scope

To understand closures, it is important to grasp the concept of lexical scope. Lexical scope means that a function's scope is determined by its physical location in the code. The scope is fixed at the time of writing and does not change when the function is called

Here's a basic example to illustrate lexical scope:

function outerFunction() {
    const outerVariable = 'I am outside!';

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

    innerFunction();
}

outerFunction(); // Logs: 'I am outside!'

Enter fullscreen mode Exit fullscreen mode

In this example, innerFunction has access to outerVariable because it is defined within the same lexical scope. But what happens when innerFunction is executed outside of outerFunction?

function outerFunction() {
    const outerVariable = 'I am outside!';

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

    return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction(); // Logs: 'I am outside!'

Enter fullscreen mode Exit fullscreen mode

Here, outerFunction returns innerFunction, which is then stored in the variable myInnerFunction. Even though outerFunction has finished executing, innerFunction still has access to outerVariable because of the closure.

How Closures Work

When a function is declared, it retains references to its surrounding scope. Even if the outer function has completed execution, the inner function can still access the variables of the outer function.

function createCounter() {
    let count = 0;

    return function() {
        count += 1;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
console.log(counter()); // Output: 3

Enter fullscreen mode Exit fullscreen mode

Use Cases of Closures

1. Data Privacy/Encapsulation by closures

Closures are often used to create private variables that are not accessible from the global scope.

function createPerson(name) {
    let age = 0;

    return {
        getName: function() {
            return name;
        },
        getAge: function() {
            return age;
        },
        incrementAge: function() {
            age += 1;
        }
    };
}

const person = createPerson('John');
console.log(person.getName()); // Output: John
console.log(person.getAge()); // Output: 0
person.incrementAge();
console.log(person.getAge()); // Output: 1

Enter fullscreen mode Exit fullscreen mode

In this example, age is a private variable, and it can only be modified using the methods provided.

2. Function Factories by Closures

Closures can be used to create function factories—functions that create other functions with pre-configured behavior.

function createGreeting(greeting) {
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeting('Hello');
const sayHi = createGreeting('Hi');

console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob')); // Hi, Bob!

Enter fullscreen mode Exit fullscreen mode

In this example, createGreeting creates a new function with a specific greeting. The returned function retains access to the greeting variable, thanks to the closure.

3. Partial Application

Partial application allows you to fix a number of arguments of a function and produce a new function.

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

const addFive = add(5);
console.log(addFive(3)); // Output: 8
console.log(addFive(10)); // Output: 15

Enter fullscreen mode Exit fullscreen mode

4. Memoization with Closures

Closures are useful in memoization, where the result of a function call is cached for future use.

function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache[key]) {
            return cache[key];
        }
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

const factorial = memoize(function(n) {
    if (n === 0) return 1;
    return n * factorial(n - 1);
});

console.log(factorial(5)); // Output: 120
console.log(factorial(5)); // Output: 120 (cached result)

Enter fullscreen mode Exit fullscreen mode

Closures Common Pitfalls and Best Practices

1. Unintended Closures

Be mindful of closures within loops, as they may lead to unexpected behavior.

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

// Output after 1 second: 3, 3, 3


// Using let instead of var fixes this issue because let has block scope.

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

// Output after 1 second: 0, 1, 2

Enter fullscreen mode Exit fullscreen mode

2. Memory Leaks because of closures

Be cautious of creating closures that retain references to large objects, which can lead to memory leaks. Ensure that closures do not unnecessarily retain references to large objects when they are no longer needed.

function createBigArray() {
    const largeArray = new Array(1000000).fill('test');

    return function() {
        console.log(largeArray[0]);
    };
}

const arrayLogger = createBigArray();

Enter fullscreen mode Exit fullscreen mode

Conclusion

Closures are a powerful feature in JavaScript that allow functions to retain access to their lexical scope even when executed outside of it. They are useful for data privacy, creating function factories, and managing state in asynchronous code.

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

...a closure is formed when a function retains access to its lexical scope..

In other words, every time a function is created - as ALL functions retain access to their lexical scope.

Collapse
 
rahulvijayvergiya profile image
Rahul Vijayvergiya

Agree,

@jonrandy If you're interested, I have a post about Lexical Scope that you might like to review.
dev.to/rahulvijayvergiya/comparing...