1. Home
image

JavaScript Tutorial Concepts - From Beginner to Pro

Learn JavaScript from scratch! Our tutorial covers basics to advanced concepts. Start learning today!

  • 24
  • 9 Hours
right-top-arrow
15

JavaScript Closure

Updated on 08/08/2024237 Views

Closures are special types of functions in JavaScript that "remember" the variables in the environment where they were created. This ability lets an inner function access these variables even after the execution of the outer functions has been completed.

Let us learn more about JavaScript closure and how we can apply it in JS programming.

Closure Concept in JavaScript

Closures can help us create private variables within functions, shielding them from outside interference. Closures are key to patterns like the module pattern, which helps structure code and create reusable components. We can use closures to build functions that maintain and update state over time, like the counter example below.

JavaScript closure example program:

Code:

function createCounter() {

  let count = 0;

  return function() {

    count++; 

    return count; 

  };

}

const counter = createCounter();

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

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

Understanding Lexical Scope

Scope in JavaScript determines where variables are accessible. Function scope means a variable declared within a function is only available inside that function. Block scope (introduced with let and const) creates even smaller scopes within blocks of code (like if statements or loops).

Hoisting Concepts

Hoisting is a quirk of JavaScript. It conceptually "lifts" var variable declarations to the top of their scope, which can lead to unexpected behavior if you're not familiar with it.

So, how does hoisting impact JavaScript closures?

  • var declarations and hoisting: With var, only the declaration of the variable is hoisted, not its assignment. This can create surprising results within closures.

Example:

function createCounter() {

    var count = 0; 

    return function() {

        console.log(count); // Might output 'undefined' at first

        count++;

        return count;

    }

}

In the above example, accessing count within the inner function might log 'undefined' initially because only the declaration of count was hoisted, not its initialized value of 0.

  • let and const: let and const were introduced to fix these hoisting-related surprises. They are block-scoped, meaning they're only hoisted within their corresponding block (e.g., an if statement or loop). Using let or const within closures provides more predictable behavior.

Example:

Code:

function createFuncWithLet() {

    for (let i = 0; i < 3; i++) {

        setTimeout(function() {

            console.log("Using let: ", i); 

        }, 1000);

    }

}

createFuncWithLet(); 

In this example, the i variable is declared with let. let has block scope. Each iteration of the loop creates a new block scope, and a new i variable is bound to that scope. The setTimeout functions close over these separate instances of i

let and const provide block scoping, offering more predictable and intuitive behavior when working with employing the use of closure in JavaScript. I think we should mostly use let and const over var to avoid hoisting confusion and ensure variables are scoped as you expect.

If possible, we should also declare variables at the top of their scope, which is helpful regardless of using var, let, or const. This makes code more readable and reduces the chance of hoisting-related surprises. I believe that understanding how hoisting interacts with variable declarations is crucial when working with closures, especially if you're using older code that may still rely on var.

How Lexical Scope Creates Closures

Lexical scope is the rulebook for variable access. Functions in JavaScript are created within a specific lexical environment. When a function closes over its lexical environment, we have a closure.

Imagine a function as a backpack. When created, it packs variables from its surrounding environment. This backpack lets the function access those specific variables even if it leaves its birthplace.

Why is this important? This type of JavaScript closure allows us to create functions that "remember" data even when their original environment is no longer active, demonstrating the power and utility of closures. Now that we know about JavaScript closure scope, let us check out an example.

Lexical scope creating closure function in JavaScript example:

Code:

function outerFunction(name) {

  let message = "Hello, " + name + "!";

  function innerFunction() {

    console.log(message);

  }

  return innerFunction;

}

let greetChris = outerFunction("Chris");

greetChris(); // Output: Hello, Chris! 

In the above example, the outerFunction has its own lexical environment, where the variable message is created. The innerFunction is defined inside of outerFunction. This is crucial, as it means innerFunction exists within the lexical scope of its parent function.

When outerFunction executes, it returns the innerFunction. This innerFunction forms a closure. Even after outerFunction finishes, if we call greetChris(), it can still access the message variable. This is despite message residing in the now-finished outerFunction's scope.

So, what did we learn from this?

The innerFunction was created within a lexical environment defined by the outerFunction. Lexical scope rules dictate that it retains access rights to that environment's variables. The innerFunction (now stored in greetChris), has essentially packed up the message variable within its backpack. It carries this closure with it wherever it goes.

Practical Applications of Closures

Let us learn all about the practical applications of closure in JavaScript with example programs.

Private Variables and Encapsulation

In JavaScript, there's no built-in way to make object properties truly private. Closures simulate private variables to promote encapsulation.

Example:

Code:

function createCounter() {

  let count = 0;

  return {

    increment: function() {

      count++;

    },

    getValue: function() {

      return count;

    }

  };

}

const counter = createCounter();

console.log(counter.count); // Undefined - 'count' is inaccessible

counter.increment();

counter.increment();

console.log(counter.getValue()); // Outputs: 2

Event Listeners

Passing variables directly to event listeners can lead to values being incorrect if the variable changes. Closures preserve the correct context within event handlers.

Example:

const buttons = document.querySelectorAll('button'); 

for (let i = 0; i < buttons.length; i++) {

  buttons[i].addEventListener('click', function() {

    // The 'i' value at the time the listener is attached is saved:

    console.log("Button clicked:", i); 

  });

}

Partial Application and Currying

We should always create more specialized functions from a generic one. Closures let us "pre-fill" some arguments.

Example:

Code:

function add(x, y) {

  return x + y;

}

const addFive = function(y) {

  return add(5, y); // Partially applies 'x' as 5

};

console.log(addFive(3)); // Outputs: 8

Asynchronous Programming

Keeping track of data associated with asynchronous operations (e.g., AJAX calls, timeouts). Closures maintain the correct state even when the asynchronous code executes later.

Example:

Code:

function getUserData(userId, callback) {

  // Simulate an asynchronous request

  setTimeout(() => {

    const userData = { id: userId, name: 'Alice' };

    callback(userData); 

  }, 1000);

}

getUserData(123, function(data) {

  console.log(data); // Outputs: { id: 123, name: 'Alice' } after 1 second

});

Additional Examples of JavaScript Closures

We have already covered the core concepts in this JavaScript closure tutorial, now let us look at some examples that can help you understand closures in more detail.

Maintaining State in a Timer

Imagine you need to build a countdown timer for a website, maybe for a quiz or game. This timer needs to accurately track time, and you'll want the ability to pause and resume it on demand. Closures offer a powerful way to maintain the timer's state, ensuring everything works seamlessly.

Example:

function createTimer(seconds) {

  let remaining = seconds;

  const intervalId = setInterval(function() {

    remaining--; 

    console.log("Time remaining:", remaining); 

    if (remaining === 0) {

      clearInterval(intervalId);

      console.log("Time's up!");

    }

  }, 1000); 

  return {

    pause: function() {

      clearInterval(intervalId);

      console.log("Timer paused.");

    },

    resume: function() {

      intervalId = setInterval(function() { 

          // Accesses 'remaining' from the closure

          // ... (same logic as before) ... 

      }, 1000);

      console.log("Timer resumed.");

    }

  };

}

const myTimer = createTimer(5); 

myTimer.pause();

// ... some time elapses ...

myTimer.resume(); 

Implementing Memoization

Let's say you have a complex calculation that takes a long time to execute. If you frequently need to recalculate with the same input values, it would be much more efficient to store the results, saving precious time. Closures provide an elegant way to achieve a "memory" for our functions, making them smarter!

Example:

function memoize(func) {

  const cache = {};

  return function(...args) {

    const key = JSON.stringify(args); 

    if (key in cache) {

      return cache[key];

    } else {

      const result = func(...args);

      cache[key] = result;

      return result;

    }

  };

}

function expensiveCalculation(x, y) { 

    // Simulates a long calculation

    for (let i = 0; i < 100000000; i++) {}

    return x * y; 

}

const memoizedCalculation = memoize(expensiveCalculation);

console.log(memoizedCalculation(2, 3)); // Long calculation the first time

console.log(memoizedCalculation(2, 3)); // Instant result from cache

If you wish to master JS and other core tools, you can check out upGrad’s full stack development courses.

Common Issues With JavaScript Closures

Now that I have managed to get JavaScript closure explained to you with the above examples, let us check out some of the common problems we face with closures.

1. Memory and Performance

  • Memory leaks (especially in older browsers): Every closure holds on to its surrounding variables. If you have many long-lived closures referencing large external variables, this can prevent those variables from being garbage collected, even when they're no longer needed. Over time, this can lead to memory consumption issues.
  • Optimization: Avoid creating closures unnecessarily within large loops or when referencing big data structures. If a closure is no longer needed, explicitly set any large external variables it holds to null to aid garbage collection. In modern browsers, consider using WeakMaps for storing references within closures. WeakMaps don't prevent garbage collection.

2. The "this" Keyword

  • this confusion: The value of this inside a function in JavaScript depends on how the function is called, not where it's defined. Closures further complicate this. For example, using a closure as an event listener, the this within the closure might refer to the element that triggered the event, not what you expect.
  • Strategies:
    • .bind(): Explicitly bind the this value you want a function to use (e.g., myFunc.bind(myObject))
    • Arrow Functions: Arrow functions lexically inherit their this value from the enclosing scope.
    • Preserve this: Store the desired this in a variable (const self = this;) and reference it within the closure.

Despite these issues being there, we should also remember that memory leaks related to closures are less frequent in modern browsers due to improved garbage collection. Also, I personally would suggest that you don't obsess about these pitfalls at the expense of code readability. Only optimize if you've measured a real performance issue.

Wrapping Up

If you wish to learn about other essential JS concepts like JavaScript closures, you can check out the other tutorials from upGrad.

If you wish to master other web development technologies such as JS and other software engineering concepts, you can enroll in one of upGrad’s software engineering courses

Frequently Asked Questions

  1. What is a closure in JavaScript?

A closure in JavaScript is the ability of inner functions to access and retain variables from their outer (enclosing) functions, even after the execution of the outer functions has been completed. This creates a private scope for the inner functions.

  1. What is closure and promise in JavaScript?

Closure provides access to an outer function's variables from an inner function, creating a persistent, private scope. Meanwhile, promise represents the eventual outcome (success or failure) of an asynchronous operation. Promises make handling asynchronous code more organized.

  1. How to avoid closure in JavaScript?

Sometimes you might want to avoid the variable-capturing behavior of closures. Here are ways to do that:

  • Use let and const
  • Pass arguments
  • Modularize with classes
  1. What are closures in programming?

The concept of closures exists in many programming languages. It generally means a function that "remembers" its surrounding variables, even if the outer context is no longer active.

  1. What is callback in JS?

A callback in JavaScript is a function passed as an argument to another function. The outer function can then execute the callback function at a later time, often after an asynchronous task is completed.

  1. Why closure is so important in JavaScript?
  • Data encapsulation: Closures help create private variables, enabling data hiding and protection.
  • State preservation: They allow functions to "remember" values between calls, useful for counters, event handlers, and more.
  • Modular code: Closures promote modularity by breaking down functionality into smaller, reusable units.
  1. Why do we need closure?

Closures exist to solve specific problems in programming, primarily the need to manage state and create a controlled scope within functions.

  1. What is the point of closure?

The point of closure is to manage states.

  1. How do you use closure in JavaScript?

We can use closures to build functions in JavaScript that remember variables from the environment of their creations.

image

mukesh

Working with upGrad as a Senior Engineering Manager with more than 10+ years of experience in Software Development and Product Management.

Get Free Career Counselling
form image
+91
*
By clicking, I accept theT&Cand
Privacy Policy
image
Join 10M+ Learners & Transform Your Career
Learn on a personalised AI-powered platform that offers best-in-class content, live sessions & mentorship from leading industry experts.
right-top-arrowleft-top-arrow

upGrad Learner Support

Talk to our experts. We’re available 24/7.

text

Indian Nationals

1800 210 2020

text

Foreign Nationals

+918045604032

Disclaimer

upGrad does not grant credit; credits are granted, accepted or transferred at the sole discretion of the relevant educational institution offering the diploma or degree. We advise you to enquire further regarding the suitability of this program for your academic, professional requirements and job prospects before enr...