Event Loop, Functions and Closure

  1. Event Loop
    In JavaScript, the event loop is a crucial concept that handles asynchronous operations. It allows the execution of code to continue without waiting for non-blocking operations like I/O, timers, and events to complete.
    The event loop is an essential part of the JavaScript runtime environment, and it ensures that the single-threaded nature of JavaScript does not lead to blocking operations.

    Here's a simplified explanation of how the event loop works,

    i) Call Stack
    JavaScript is single-threaded, meaning it can only execute one operation at a time. The call stack keeps track of the currently executing function and its context.

    ii) Callback Queue
    Asynchronous operations, like API requests, timers, or user interactions, are handled outside the main execution thread. When these operations complete, their callbacks are pushed into the callback queue.

    iii) Event Loop
    The event loop constantly checks the call stack and the callback queue.

    If the call stack is empty, the event loop takes the first callback from the queue and pushes it onto the call stack, allowing it to execute.
    Here's a basic example to illustrate the event loop using setTimeout,

     console.log("Start");
    
     setTimeout(() => {
       console.log("Inside setTimeout callback");
     }, 2000);
    
     console.log("End");
    

  2. Callback Function
    In JavaScript, a callback function is a function that is passed as an argument to another function, and it is expected to be executed after the completion of a specific task or at a certain event. Callback functions are commonly used in asynchronous operations, such as handling events, making API calls, or performing file I/O.

     // Example of a callback function
     const greet = (name, callback) => {
       console.log(`Hello, ${name}!`);
       callback(); // Call the callback function after greeting
     }
    
     // Callback function definition
     const sayGoodbye = () => {
       console.log("Goodbye!");
     }
    
     // Using the greet function with the sayGoodbye callback
     greet("John", sayGoodbye);
    

    In this example, the greet function takes two parameters: name and callback. The greet function logs a greeting message and then calls the callback function. In this case, sayGoodbye is the callback function passed to greet. When greet is called with "John" and sayGoodbye, it logs "Hello, John!" and then "Goodbye!".
    Callback functions are particularly useful in scenarios where you want to perform certain actions only after a specific task or event has completed, especially in asynchronous JavaScript code. They are extensively used with functions like setTimeout, setInterval, and in handling asynchronous operations like API requests or reading files.

  3. Asynchronous Programming
    In JavaScript, asynchronous programming is crucial for handling tasks that may take some time to complete, such as network requests, file I/O, or other operations that could cause delays. Asynchronous functions are functions that operate asynchronously, allowing other code to run while they are still processing.
    There are several ways to work with asynchronous code in JavaScript. Here are some common patterns,

    i) Callbacks
    Using callback functions is one of the oldest ways to handle asynchronous code.

    ii) Promises
    Promises provide a cleaner and more structured way to handle asynchronous code.

    iii) Async/Await
    Async functions and the await keyword provide a more synchronous-looking way to write asynchronous code.

    Note that an async function always returns a promise, and the await keyword can only be used inside an async function.

    Choose the method that best fits your needs and the JavaScript version you are working with. Promises and async/await are generally considered more modern and readable compared to callbacks.

  4. First Class Functions
    In JavaScript, functions are first-class citizens, which means that functions can be treated like any other variable, such as numbers, strings, or objects. Here are some characteristics of first-class functions in JavaScript.

    i) Assigning to Variables - You can assign a function to a variable.

     const myFunction = function() {
       console.log("Hello, world!");
     };
    

    ii) Passing as Arguments - You can pass functions as arguments to other functions.

     function greet(callback) {
       callback();
     }
     greet(myFunction); // Prints: Hello, world!
    

    iii) Returning from Functions - Functions can also be returned from other functions.

     function createGreeter() {
       return function() {
         console.log("Greetings!");
       };
     }
    
     const greeter = createGreeter();
     greeter(); // Prints: Greetings!
    

    v) Creating on the Fly - Functions can be created on the fly, often referred to as anonymous functions or function expressions.

     const multiply = function(x, y) {
       return x * y;
     };
     console.log(multiply(3, 4)); // Prints: 12
    
  5. Higher-Order Functions
    Functions that take other functions as arguments or return functions are called higher-order functions. We can say that Higher Order Function accepts the First Class Function (i.e. callback function) as a parameter.

     function operation(x, y, callback) {
       return callback(x, y);
     }
    
     // here operation() is the Higher order function
     const result = operation(5, 3, multiply);
     console.log(result); // Prints: 15
    
  6. Currying
    Currying is a technique in functional programming where a function is transformed into a sequence of functions, each taking a single argument. In JavaScript, currying allows you to create more specialized and reusable functions. Here's an example of currying in JavaScript,

     // Non-curried function
     function add(x, y, z) {
       return x + y + z;
     }
     console.log(add(2, 3, 4)); // Prints: 9
    
     // Curried version
     function curryAdd(x) {
       return function(y) {
         return function(z) {
           return x + y + z;
         };
       };
     }
    
     const curriedAdd = curryAdd(2);
     const addResult = curriedAdd(3)(4);
     console.log(addResult); // Prints: 9
    

    In this example, the curryAdd function takes an argument x and returns a function that takes y, which in turn returns another function that takes z. You can then invoke these functions step by step to achieve the same result as the non-curried version.
    Currying can also be achieved using modern JavaScript syntax. Here's an example using arrow functions,

     const curryAdd = x => y => z => x + y + z;
    
     const curriedAdd = curryAdd(2);
     const addResult = curriedAdd(3)(4);
     console.log(addResult); // Prints: 9
    

    Using a curried function, you can partially apply arguments and create new functions, making it easier to reuse and compose functions in a more modular way. This can be particularly useful in scenarios where you want to create variations of a function with fixed parameters.