Closure

# Closure

# What is Closure ?

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

  • When a function is defined inside another function, a closure is created. The inner function retains a reference to the variables and scope of its outer function.
  • When the outer function finishes executing and returns, the closure is still intact with its captured variables and scope chain.
  • The closure allows the inner function to access and manipulate the variables of its outer function, even if the outer function’s execution has completed.
  • This behavior is possible because the closure maintains a reference to its outer function’s variables and scope chain, preventing them from being garbage collected.

In simpler terms, a closure can "remember" values from its outer function and use them later, even if the outer function has returned and those values would normally be out of scope.

# When to use closure ?

# Encapsulation

Closures allow you to create private variables that cannot be accessed directly from outside the function. This helps in maintaining the integrity and security of the data.

const makeCounter = () => {
    let count = 0;

    return () => {
        count++;
        console.log(count);
    };
};

let counter = makeCounter();
counter(); // logs 1
counter(); // logs 2
counter(); // logs 3

Here we create a closure with the arrow function being returned that retains reference of the count variable. The count variable is not exposed outside of the returned function so it's effectively a private variable that can only be modified by calling makeCounter function.


# Factory Functions / Module

Closures enable you to write factory functions that can produce objects with similar methods but maintaining their own private state.

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log(something);
    }
    function doAnother() {
        console.log(another.join(" ! "));
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother,
    };
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

# Callbacks and Event Handling

Closures are used in callbacks, especially in asynchronous code, where you might want to preserve certain variables or state across asynchronous operations.

Here is an example using the fs module to read files asynchronously.

const fs = require("fs");

function readFiles(filenames, callback) {
    filenames.forEach((filename, index) => {
        let metadata = `File number ${index + 1}`;

        fs.readFile(filename, "utf-8", (err, data) => {
            if (err) {
                console.error(`Error reading ${filename}: ${err}`);
                return;
            }

            // Closure captures 'filename' and 'metadata'
            callback(filename, metadata, data);
        });
    });
}

// Usage
const filenames = ["file1.txt", "file2.txt", "file3.txt"];

readFiles(filenames, (filename, metadata, data) => {
    console.log(`Reading ${filename} (${metadata}):`);
    console.log(data.substring(0, 100));
});

In this example, the readFiles function accepts a list of filenames and a callback function. For each filename, it kicks off an asynchronous read operation (fs.readFile). The metadata and filename variables are captured in the closure for the callback passed to fs.readFile.

This ensures that even when the asynchronous file read operation completes at some later time, the filename and metadata variables are still accessible, allowing us to associate the correct metadata and filename with each file's content.


# Functional Programming

Closures are fundamental in functional programming patterns, enabling higher-order functions and partial application.

Partial application: the process of fixing a number of arguments to a function, generating a new function with fewer remaining arguments. It allows us to create new functions from existing ones by pre-specifying some of the arguments which can lead to more modular and reusable code.

// Partial application
function sum(a, b, c) {
    return a + b + c;
}
// fixing one of the argument to be 1
function partialSum(b, c) {
    return sum(1, b, c);
}

console.log(partialSum(2, 3)); // 6

# Memoization

Closures can be used to implement memoization, where results of expensive function calls are cached and returned when the same inputs occur again.

A classic example is calculating fibonacci numbers. The inner fib function retains the reference to the cache variable and keeps accessing and modifying it in subsequent recursive calls.

function memoizedFibonacci() {
    const cache = {}; // Cache to store computed Fibonacci numbers

    return function fib(n) {
        // Check if value for n is already computed and cached
        if (n in cache) {
            return cache[n];
        }

        // Compute and cache the Fibonacci number if it's not in cache
        if (n <= 1) {
            return n;
        }

        cache[n] = fib(n - 1) + fib(n - 2);
        return cache[n];
    };
}

const memoizedFib = memoizedFibonacci();

console.log(memoizedFib(10)); // Output will be 55
console.log(memoizedFib(10)); // Output will be 55, but this time it's retrieved from cache