blog post

Understanding JavaScript Closures

Closures are a fundamental concept in JavaScript, that can help you manage scope, data privacy, and memory efficiency. Understanding closures is crucial for coding and allow to prevent potential bugs in the code.

What is a Closure?

A closure is a form of lexical scoping used to preserve variables from the outer scope of a function in the inner scope of a function. In lexical scoping - the scope of a variable is determined by its position within the source code.

A closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

This unique property allows closures to maintain state between function invocations.

Let's explore an example with a closure withing a nested function:

function outerFunction() { const text = "Hello World!"; function print() { console.log(text); } print(); } outerFunction(); // Output: "Hello World!"

Here a print() function captures a text variable fom the outer function, and this outer function is a closure for a print() function.

Why Use Closures?

Closures are particularly useful for:

  • Data encapsulation: Closures can hide variables from the global scope, thus protecting data integrity and avoiding namespace pollution.
  • Creating private variables: In JavaScript, closures provide a way to create private variables that can only be accessed by privileged methods.

Let's explore an example on how to create private variables with closures:

function createCounter() { let count = 0; return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); } }; } const counter = createCounter(); counter.increment(); // Output: 1 counter.decrement(); // Output: 0 // Can't access here - counter.count

Here a counter object uses closures to keep count variable private, exposing only the increment and decrement methods.

Closures and Scoping in JavaScript

Scoping determines where in your code variables and functions can be accessed. JavaScript has three primary types of scope:

  • Global Scope: A variable declared in the global scope is accessible from anywhere in your code.
  • Function Scope: Variables declared inside a function are only accessible within that function and by closures.
  • Block Scope: Introduced with ES6 through let and const, block-scoped variables are accessible only within the block (delimited by {}) they are declared in.

The closure can access variables from the following sources:

  • Its own scope (variables defined between its curly brackets)
  • The outer function's scope
  • The global scope

To declare a variable you can use var, let, and const that affects both scope and closure:

  • var has function scope. Variables declared with var are hoisted to the top of their function scope, meaning they can be referenced before they are declared without causing a ReferenceError, but will return undefined until the actual declaration line is executed. You can re-declare the same variable multiple times within its scope without errors.
  • let has block scope. Block is what is placed inside the curly braces { }, it can be if statement, switch, for, etc. let variables can't be accessed before declaration.
  • const has also block scope, but it's value can't be changed after declaration.

For more code safety it is preferred to use const everytime when the value doesn't need to be changed; otherwise, use let. Avoid using var as it can be re-declared multiple times with different types and has a wider visibility scope because of hoisting to the top of declared function. If a var variable is declared outside the function - it is placed in the global scope and can be accessed anywhere.

Let's explore an examples on how closures work when using variables declared using var and let:

function varFunction() { var functions = []; for (var i = 0; i < 3; i++) { functions.push(function() { console.log("Using var: Index is", i); }); } return functions; } var func = varFunction(); func[0](); // Outputs: "Using var: Index is 3" func[1](); // Outputs: "Using var: Index is 3" func[2](); // Outputs: "Using var: Index is 3"

Here, each function is created in the loop shares the same function-scoped i. By the time the functions are called, the loop has finished and the value of i is 3, which is why "3" is logged for all the function calls.

function letFunction() { var functions = []; for (let i = 0; i < 3; i++) { functions.push(function() { console.log("Using let: Index is", i); }); } return functions; } var func = letFunction(); func[0](); // Outputs: "Using let: Index is 0" func[1](); // Outputs: "Using let: Index is 1" func[2](); // Outputs: "Using let: Index is 2"

Here, let creates a new block-scoped variable i for each iteration of the loop. Each function in the array captures its own i, so when they are called, they log the index value at the time of that particular iteration. That's why in this example we have correct indexes printed to console for each function call.

Closure Best Practices

  • Minimize closure size: Only capture necessary variables to reduce memory overhead. Closures can lead to memory leaks if not used carefully. Since closures hold references to outer scope variables, these variables cannot be garbage collected as long as the closure exists.
  • Avoid creating closures with var variables inside loops: This can lead to unexpected behavior and inefficiencies.
  • Use closures judiciously: Consider alternative patterns if closures lead to increased complexity or performance issues.

Hope you find this blog post useful. Happy coding!

After reading the post consider the following:

  • Subscribe to receive newsletters with the latest blog posts

  • Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)

If you like my content - consider supporting me

Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!