Javascript Closures

Real life Application of Closures.

Javascript Closures

In JavaScript, a closure is created when a function is defined within another function, and it has access to the outer function's variables. This means a closure can remember and access its lexical scope (the variables in its parent function) even when the parent function has finished executing.

It has three Scope Chains.

  • The first one is in Curly brackets,

  • The second one is in the Outer Scope of the Curly brackets, and

  • The third one is in the Global Scope.

Here's a simple example:

function outerFunction() {
  let outerVariable = "I am from the outer function";

  function innerFunction() {
    console.log(outerVariable); // innerFunction can access outerVariable due to closure
  }

  return innerFunction;
}

const closureExample = outerFunction();
closureExample(); // Output: "I am from the outer function"

In this example, innerFunction is a closure because it's defined inside outerFunction and has access to outerVariable, which is outside of its scope. When outerFunction is called, it returns innerFunction, and when closureExample is invoked, it still has access to outerVariable due to the closure, even though outerFunction has already finished executing.

So, a closure in JavaScript is essentially a function along with its lexical scope, allowing the function to access variables from its parent scope even after the parent function has completed its execution. This concept is powerful and frequently used in JavaScript programming.

Real-Life Application: Building a Task Management System with Advanced Functionality

Imagine you're tasked with developing a robust task management system for a project management tool. This system must handle various features efficiently, such as asynchronous operations, memoization, and dynamic task prioritization. Closures will play a crucial role in achieving these functionalities.

1. Module Design Pattern with Closures:

You can use closures to create encapsulated modules for different aspects of your task management system. Here's an example of a module that handles task creation and manipulation:

const TaskManager = (function() {
    const tasks = []; // Private array to store tasks

    return {
        addTask: function(task) {
            tasks.push(task);
        },
        getTasks: function() {
            return tasks;
        }
    };
})();

TaskManager.addTask("Design feature X");
TaskManager.addTask("Implement task management");
console.log(TaskManager.getTasks()); // Output: ["Design feature X", "Implement task management"]

In this example, the TaskManager module is created using a closure, providing private encapsulation of the tasks array.

2. Currying for Dynamic Task Prioritization:

If you don't know about Currying, In simpler terms, currying is breaking down a function that takes multiple arguments into a series of functions, each taking a single argument.

For example, consider a function add(a, b, c) which adds three numbers. After currying, it becomes add(a)(b)(c), where you can call it with one argument at a time.

Currying is useful for creating more specialized and reusable functions, enhancing code readability, and enabling the creation of higher-order functions.

Now, You can utilize currying to create functions that prioritize tasks dynamically based on different criteria. Here's an example of a curried function that prioritizes tasks based on their importance:

function prioritizeTask(importance) {
    return function(task) {
        return { task, importance };
    };
}

const highPriorityTask = prioritizeTask('High')('Fix critical bug');
console.log(highPriorityTask); // Output: { task: 'Fix critical bug', importance: 'High' }

In this example, prioritizeTask is a curried function that takes importance as a parameter and returns another function. This curried function allows for dynamic task prioritization.

3. Function Running Only Once:

You can create a function that runs only once using closures. This is often used for initialization tasks. Here's an example:

function initializeApp() {
    let initialized = false;

    return function() {
        if (!initialized) {
            console.log("Initializing the application...");
            initialized = true;
        } else {
            console.log("Application is already initialized.");
        }
    };
}

const runAppOnce = initializeApp();
runAppOnce(); // Output: Initializing the application...
runAppOnce(); // Output: Application is already initialized.

In this example, initializeApp it returns a closure that ensures the application initialization code runs only once.

4. Memoization for Performance Optimization:

Memoization is a closure-based technique for optimizing functions by caching their results. Let's consider a function that calculates Fibonacci numbers using memoization:

function fibonacci() {
    const memo = {};

    return function(n) {
        if (n in memo) {
            return memo[n];
        } else {
            if (n <= 2) return 1;
            memo[n] = fibonacci(n - 1) + fibonacci(n - 2);
            return memo[n];
        }
    };
}

const fib = fibonacci();
console.log(fib(10)); // Output: 55 (calculated efficiently with memoization)

In this example, the fibonacci function returns a closure that uses the memo object to store previously calculated Fibonacci numbers, optimizing performance by avoiding redundant calculations.

5. Maintaining State in an Asynchronous Environment:

In an asynchronous application, closures can help maintain state across asynchronous operations. Consider an example where tasks are fetched asynchronously, and their completion status is updated:

function TaskManager() {
    const tasks = [];

    return {
        addTask: function(task) {
            tasks.push({ task, completed: false });
        },
        completeTask: function(index) {
            setTimeout(function() {
                tasks[index].completed = true;
                console.log(`Task "${tasks[index].task}" completed.`);
            }, 1000);
        }
    };
}

const taskManager = TaskManager();
taskManager.addTask("Write documentation");
taskManager.addTask("Test user authentication");

taskManager.completeTask(0); // Output: Task "Write documentation" completed. (after 1 second)
taskManager.completeTask(1); // Output: Task "Test user authentication" completed. (after 1 second)

In this example, the TaskManager closure maintains the state of tasks across asynchronous operations. The completeTask method is used setTimeout to simulate asynchronous task completion.

6. Iterators for Task Iteration:

You can use closures to create custom iterators for iterating through tasks. Here's an example:

function TaskIterator(tasks) {
    let index = 0;

    return {
        next: function() {
            if (index < tasks.length) {
                return { value: tasks[index++], done: false };
            } else {
                return { done: true };
            }
        }
    };
}

const tasks = ["Design UI", "Implement backend", "Write tests"];
const taskIterator = TaskIterator(tasks);

let nextTask = taskIterator.next();
while (!nextTask.done) {
    console.log(nextTask.value); // Output: Design UI
    nextTask = taskIterator.next();
}

In this example, TaskIterator returns a closure with a next method, allowing iteration through tasks.

Conclusion:

Closures are a powerful tool in JavaScript, enabling the creation of modular, encapsulated, and efficient code. In this real-life application scenario for an SDE II level, closures are used extensively to implement module patterns, currying, memoization, maintaining state in asynchronous operations, managing iterative processes, and more. Understanding and effectively utilizing closures in such complex applications are key skills for an experienced software engineer.

A closure would be a function defined inside another function that captures variables from the outer function's scope. Let me correct the example to demonstrate a closure:

// Function factory to create requireAuth middleware.
function createRequireAuth(authenticate) {
    // Middleware function to check user authentication
    return function requireAuth(req, res, next) {
        const isAuthenticated = authenticate(req); // Function to check user authentication status

        if (isAuthenticated) {
            // If user is authenticated, proceed to the next middleware or route handler
            next();
        } else {
            // If user is not authenticated, send an unauthorized response
            res.status(401).send('Unauthorized access');
        }
    };
}

// Example authentication function (for demonstration purposes)
function checkUserAuthentication(req) {
    // Logic to check user authentication (e.g., validate JWT token)
    const authToken = req.headers.authorization;
    return authToken === 'validAuthToken';
}

const requireAuth = createRequireAuth(checkUserAuthentication);

// Express.js route handler using the requireAuth middleware
app.get('/secure-route', requireAuth, (req, res) => {
    res.send('Welcome to the secure route!');
});

In this corrected example:

  1. createRequireAuth is a function factory that returns the requireAuth closure.

  2. requireAuth is a closure because it is defined inside another function (createRequireAuth) and captures the authenticate function from its outer scope.

  3. The authenticate function is passed as a parameter to createRequireAuth and is used inside the requireAuth closure.

Now, requireAuth is a closure that captures the authenticate function from the outer scope, making it a true example of a closure in JavaScript. This pattern is often used to create reusable middleware functions with different authentication methods.

(Bouns):

Here is some extra stuff through which you can remember Closure:

Different Snacks (Variables/Data): Inside the lunchbox, there are different snacks. Each snack represents a variable or a piece of data you want to store.

Magic Lid (Function): The lunchbox comes with a magic lid (function). When you close the lid, it remembers the snacks inside. Later, even if you move to a different place (outside the function), you can open the lunchbox, and all your snacks are still there, just as you left them.

In this simple analogy:

  • The Lunchbox (Closure): keeps your snacks (variables/data) safe.

  • Different Snacks (Variables/Data): are the items you want to store.

  • The Magic Lid (Function): remembers your snacks and keeps them safe, no matter where you go.

So, closures are like magical lunchboxes that hold your snacks and remember them, allowing you to access your goodies whenever you want, wherever you are!