Understanding JavaScript Closures: A Deep Dive into a Fundamental Concept

Understanding JavaScript Closures: A Deep Dive into a Fundamental Concept

A Beginner's Guide to JavaScript Closures

·

6 min read

In JavaScript, closures are a potent yet frequently misconstrued concept. Numerous developers view closures as one of the most challenging JavaScript concepts to grasp. However, I firmly believe that with the right explanations, understanding closures is not as intimidating as it might initially seem. This article offers essential insights to deepen your understanding of JavaScript closures, providing you with fundamental information and some use cases to further clarify this concept.

Prerequisite

To make the best out of this article, you should have the basic knowledge of the following JavaScript concepts:

  • JavaScript execution context

  • Scope-chain

  • Nested functions

A code editor and access to a modern web browser for testing and running JavaScript code snippets are also recommended.

What is JavaScript Closure?

To get started, it's crucial to understand that closures are not intentionally created entities like arrays or functions. Instead, closures naturally emerge in specific situations; your role as a developer is to recognize and appreciate these contexts.

Have you ever wondered how certain operations in JavaScript programming seem almost magical? Consider this scenario, below is a code snippet of a function responsible for updating the number of users on a certain website each time a new user signup:

const userCounter = function () {
  // Number of initial users
  let user = 0

  // Function to increment number of user on signup
  const increment = function () {
    user++
    console.log(`${user} users signed up`)
  }
  return increment
}
// Store the result of calling userCount in signup variable
const signup = userCounter()

// Call signup() 3 times
signup()
signup()
signup()

In this example, userCounter is a function that returns another function (increment). increment updates the user variable defined in its parent function each time the signup function is called, as seen from the resulting console log below:

If you pause to contemplate, a puzzling question might arise: How can the increment function, now the signup function (the return result of calling userCounter), somehow manage to access and modify the user variable defined inside the userCounter function, even though userCounter has already completed its execution? It may seem perplexing at first. How can a function reach back to variables from a parent function that has already finished its task? As you might guess, the answer lies in a powerful concept: closure.

Closure is the key that unlocks this mystery. JavaScript closure bestows a function with the ability to preserve a link to its parent function's variables, allowing continued access even after the parent function has fulfilled its purpose. This mechanism ensures that a function can always reach back and connect to the variables within its original scope. Essentially, closure acts as an invisible thread, weaving a lasting connection between a function and its parent's variables.

To illustrate this concept, consider the analogy of a family. Imagine the userCounter function as a family and the increment function as a child within this family. Closure ensures that this child (represented by the increment function) doesn't lose touch with its siblings (the user variable) even when the family (the userCounter function) travels elsewhere. In this analogy, the enduring bond between the child and its siblings symbolizes how the child functions 'close over' their parent's scope or variable environment. This unique ability allows child functions to carry this environment with them, ensuring they can access and utilize it indefinitely, creating a seamless connection that persists over time.

Common use cases of Closures

To enhance your comprehension of closures, examine these scenarios where closures naturally occur. Understanding these instances will enable you to identify closures in your code effectively.

Scenario 1: Reminder App

Imagine developing a reminder application where users can set personalized reminders with specific messages. The application then notifies users after a designated time interval." This example demonstrates that creating a closure doesn't always require returning a function from another function.

function createReminder(message, time) {

  function showReminder() {
    console.log('Reminder: ' + message)
  }
// Register and call the showReminder function after the specified time interval
  setTimeout(showReminder, time)

  console.log('Reminder set for ' + time + ' milliseconds.') // second console log
}

// Creating reminders
createReminder('Meeting with Client', 3000) // Sets a reminder after 3 seconds
createReminder('Submit Report', 5000) // Sets a reminder after 5 seconds

In this scenario, the createReminder function takes two parameters: message (the reminder message) and time (the time interval in milliseconds). showReminder is a child function of createReminder. The setTimeout function is used to schedule the showReminder function after the specified time interval. When the time elapses, the showReminder function is executed, displaying the reminder message in the console.

When you run the code and observe the console, you'll notice that immediately after the createReminder function is called, the setTimeout function runs and registers the showReminder function, followed by the second console log. Then, after the specified time intervals (3000 and 5000 milliseconds), the showReminder function is executed. It's important to note that the showReminder function runs 3 seconds and 5 seconds after its parent function (createReminder) has completed execution. Despite this, the showReminder function can access all the variables (message and time) from the variable environment in which it was created. This phenomenon indicates the presence of a closure. The fact that the showReminder function can access variables defined inside the createReminder function, even after createReminder has finished executing, unequivocally illustrates the existence of a closure.

Scenario 2: Implementing Private Variables for User Objects

Think about developing a user management system that requires the creation of user objects with sensitive information like usernames and passwords. You want to ensure that this sensitive information remains private and can only be accessed through specific methods.

function createUser(username, password) {
  // private variables
  let privateData = {
    username,
    password,
  }

  // methods that acess the private data
  return {
    getUsername: function () {
      return privateData.username
    },
    getPassword: function () {
      return privateData.password
    },
  }
}

const user2 = createUser('Tonyfed', 'pass1234')
console.log(user2.getUsername()) // Output: "Tonyfed"
console.log(user2.getPassword()) // Output: pass1234

In this scenario, privateData is encapsulated within the createUser function, making it inaccessible from outside its parent scope. The inner functions (getUsername and getPassword) have access to these private variables even after the createUser function has long been executed due to closures, thus enabling you to maintain strict control over access to sensitive user data.

Conclusion

Closures stand as a fundamental concept in JavaScript, as demonstrated throughout this article. I trust that you now can recognize closures in action within your code. Understanding closures empowers you to unravel the mysteries behind seemingly magical occurrences in your programming. By grasping this concept, you gain better control over your codebase, comprehending the reasons behind certain behaviors.

To elevate your expertise further and enhance your comfort level with javaScript closures, I encourage you to explore their potential pitfalls. This aspect, unfortunately, isn't covered in this article, but delving into the challenges associated with closures will deepen your understanding and refine your programming skills.