Closure in JavaScript

Video Tutorial
FREE
Closures in JavaScript thumbnail
This video belongs to
JavaScript Course With Certification: Unlocking the Power of JavaScript
9 modules
Certificate
Topics Covered

Closure in Javascript can be defined as the bundle of function along with its lexical environment. It is created every time a function is created. To make it more intuitive and easy to remember, we can say that every function in Javascript remembers its origin. In this article, we will delve into what is closure in JavaScript.

Lexical scoping

  • In the example, we saw that the outer scope variable is easily accessible to the inner scope. This is possible because of lexical scoping.
  • In the above example, the lexical scope of newFunction consists of the lexical scope of demoFunction and the global scope.
  • In easy terms, lexical scoping means that inside an inner scope, you can use the variable of the outer scope. It is called lexical scope as the engine determines the scopes during the lexing time.

a) Scoping with let and const

let and const keywords were introduced in ECMAScript 6 (ES6) to provide block-scoping, unlike var, which has function-level scope. Variables declared with let and const are scoped to the nearest enclosing block, a function, loop, or any other block of code.

Example:

In this example, localVar is declared using let inside the if block. It is scoped to that block, and any attempt to access it outside the block will result in a reference error. This demonstrates the block-scoping behaviour of variables declared with let.

Similarly, variables declared with const also have block-scoping and cannot be reassigned after initialization. const variables must be assigned a value at the time of declaration.

Closure in JavaScript

Now we will learn, what is closure in JavaScript?

Closure in JavaScript is a powerful concept which allows the functions to retain access to the variables from their lexical scope even after the outer function has finished executing. This means that inner functions have access to the variables and parameters of their outer function's scope, even after it has returned. This behaviour is due to the fact that functions in JavaScript form closures around the data they reference.

a) Example of Closure in JavaScript

Let's consider a simple example to illustrate closure in JavaScript:

In this example, innerFunction is defined inside outerFunction, and it has access to the outerVariable even after outerFunction has finished executing. This is possible because of closure.

b) Example with a makeAdder function

Now, let's explore a more practical example using a makeAdder function, which creates and returns another function that adds a given value to its argument:

In this example, makeAdder returns a function that adds a value x to its argument y. The returned function retains access to the x value even after makeAdder has finished executing. This allows us to create multiple functions (add5 and add10 in this case) that each add a different value to their argument, all using the same makeAdder function.

These examples demonstrate how closures in JavaScript work and how they can be used to create functions with persistent access to their enclosing scope's variables.

Practical closures

Closures in Javascript serve as powerful tools in programming by allowing functions to retain access to their lexical environment, associating data with the function's behavior. This concept draws exciting parallels to object-oriented programming, where objects encapsulate data (properties) along with methods.

In essence, closures offer a similar capability to objects with a single method, enabling developers to achieve similar outcomes in terms of data encapsulation and behavior.

HTML:

CSS (styles.css):

JavaScript (script.js):

In this example, we have three buttons (Blue, Green, and Red), each associated with the function, which is responsible for changing the background colour of the body element to the corresponding colour when clicked. These functions are created using the makeColorChanger function, which returns a closure capturing the color parameter.

Emulating private methods with closures

Emulating private methods with closures involves creating functions within a closure that are inaccessible from outside the closure, effectively encapsulating them and making them private. This technique is commonly used in JavaScript to achieve data encapsulation and protect sensitive or internal functionality from external access.

Here's a simple example demonstrating how private methods can be emulated using closures:

In this example, the function createCounter returns an object with three methods: increment, decrement, and getCount. These methods are defined within the closure of createCounter, making the count variable inaccessible from outside. This encapsulation ensures that the count variable remains private and can only be manipulated through the exposed methods.

Closure scope chain

  • The closure scope chain in JavaScript consists of three levels: the local scope, enclosing scope, and global scope.
  • Variables are first searched for within the local scope of the closure, then in any enclosing scopes, and finally in the global scope if not found locally or in the enclosing scopes.
  • This mechanism allows closures to access variables defined in their outer lexical environments, facilitating encapsulation and data privacy.

Local scope

  • This is the innermost scope within a closure where variables are declared using the let, const, or var keywords.
  • These variables are accessible only within the block or function in which they have been defined.
  • In this example, outerFunction defines a local variable localVar.
  • Inside outerFunction, innerFunction is defined as having access to localVar.
  • When innerFunction is called inside outerFunction, it can access and log the value of localVar from its own scope.

Enclosing scope

  • Enclosing Scope (Outer Scope): Also known as the outer lexical environment, this scope comprises variables declared in the outer function or block surrounding the closure.
  • When a variable is referenced within a closure but is not found in its own scope, JavaScript looks up the scope chain to find the variable in enclosing scopes until it reaches the global scope.

Explanation

  • outerFunction defines a variable outerVar and then defines innerFunction inside it.
  • innerFunction is returned from outerFunction and assigned to the variable closure.
  • When closure is called outside of outerFunction, it still has access to outerVar due to closures, even though outerFunction has finished executing.

Global scope

  • This is the outermost scope in JavaScript, encompassing all other scopes.
  • Variables which has been declared outside any function or block which have global scope and can be accessed from anywhere in the codebase.
  • However, it's important to note that variables declared with var in global scope are attached to the global object (window in the browser, global in Node.js), while those declared with let or const are not.

Explanation:

  • globalVar is declared in the global scope.
  • Inside outerFunction, innerFunction is defined, which can access globalVar from the global scope.
  • Similar to the previous example, closure retains access to globalVar even after outerFunction has finished executing, demonstrating closure's ability to access variables from the global scope.

Creating closures in loops: A common mistake

Creating javascript closures in loops can lead to unexpected behaviour if not handled properly. One common mistake is assuming that each closure will capture the value of its iteration variable at the time of creation. However, due to JavaScript's lexical scoping and variable hoisting, all closures created within a loop actually capture the same variable and its final value after the loop ends.

Consider the following example:

In this example, we expect each closure to capture the value of i at the time it was created, resulting in the output 1, 2, 3, 4, and 5. However, due to the asynchronous nature of setTimeout, the loop has already finished by the time the closures execute, and the value of i become6. As a result, all the closures print 6, instead of the expected values.

To avoid this issue, we need to generate another scope for every loop iteration using an immediately invoked function expression (IIFE) or let or const instead of var for the loop variable. This ensures that each closure captures a unique value of the loop variable at the time of creation.

In this modified example, we use an IIFE to create an updated scope for every loop iteration. The value of i is passed as an argument to the IIFE, ensuring that each closure captures the correct value of i.

Performance considerations

Performance considerations are crucial when using closures in JavaScript, as they can impact memory usage and execution speed if not handled properly. Let's delve into some examples and explanations to understand these considerations better.

Example 1: Memory Usage

In this example, the closure closure maintains a reference to the bigData array, even after the createClosure function has finished executing. This can increase memory usage if bigData contains a large amount of data. If closures like this are created frequently or are long-lived, they can contribute to memory leaks and degrade the performance of the application over time.

Explanation:

  • The createClosure function creates a closure that captures the bigData array.
  • Even after createClosure returns, the closure maintains a reference to bigData, preventing it from being garbage-collected.
  • If createClosure is called multiple times or if the closure is stored in a long-lived object or event handler, it can lead to excessive memory usage and potential memory leaks.

Example 2: Execution Speed

In this example, the performHeavyTask function performs a computationally intensive task. When this function is called inside createClosure to initialize the closure, it adds overhead to the creation and execution of the closure. If closures like this are created frequently or are executed frequently, it can lead to slower performance and decreased responsiveness of the application.

Explanation:

  • The performHeavyTask function performs a heavy computation, such as a loop with a large number of iterations.
  • This heavy computation is performed every time createClosure is called to initialize the closure.
  • If createClosure is called frequently or if the closure is executed frequently, the overhead of performing the heavy computation multiple times can impact the performance of the application.

These are Performance Consideration we should consider:

  1. Minimize Closure Creation: Avoid creating closures unnecessarily or excessively, especially if they capture large amounts of data or perform heavy computations. Instead, consider alternative solutions that do not rely on closures, such as using plain functions or optimizing data structures.

  2. Release Unused Closures: Ensure that closures are released when they are no longer needed to prevent any memory leaks and excess memory usage. This can be achieved by removing references to closures or by using techniques such as object pooling or lazy initialization.

  3. Optimize Closure Execution: If closures perform computationally intensive tasks, consider optimizing their execution by caching results, memoization, or precomputing values where possible. This can reduce the overhead of closure execution and overall improves the overall performance of the application.

By carefully considering these performance considerations and optimizing the usage of closure in JavaScript applications, developers can ensure that closures enhance the clarity, flexibility, and performance of their code without introducing unnecessary overhead or performance bottlenecks.

Conclusion

  • Closures in JavaScript are essential for retaining access to variables from their lexical scope even after executing the outer function.
  • They enable data encapsulation and behaviour association.
  • Closures offer benefits such as creating private variables and methods, supporting higher-order functions, and enabling functional programming paradigms.
  • However, developers must consider performance implications when using closures.