Table of content:

Closures are one of the most confusing concepts for new JavaScript learners. They are very powerful. Once you understand them well, your JavaScript skills will move to the next level.

In this article, I will give you all the knowledge you need to understand closures well. We will start with defining what scoping is and the different types of scoping. Then, we will learn the different ways to define variables in JavaScript. And we will move in small steps until we see how closures are defined. At the end of this article, I will show 5 different practical applications of closures that you can use now in your JavaScript code.

What is scoping?

In simple terms, scoping is the rules that determine how a variable (or function or class) is evaluated and accessed.

For example, we say that this variable's scope is some function or some if statement, and sometimes we say that its scope is global, meaning we can access it and modify it from anywhere.

Here are a couple of scoping examples.

// a is global variable
var a = 5

function fun() {
  // b's scope is only within the function fun
  let b = 'foo'
  
  if (b === 'foo') {
    // c's scope is only within this if statement
    let c = 'bar'
  }
}

Static (lexical) vs. dynamic scoping

We always hear that JavaScript is a lexical scoping language, but it's not clear what that means.

To understand what that means, we need to understand the alternative: dynamic scoping.

In some old languages, like Lisp and Bash, dynamic scoping was used. However, most modern languages stopped using it because dynamic scoping can lead to unpredictable behavior.

It can lead to that because variable scopes are determined based on the calling context at runtime. Let me make this clear with an example.

Take a look at this code:

let v = 10

// Called by b()
function a() {
  return v
}

// b() has its own variable named as v and calls a()
function b() {
  let v = 20
  return a()
}

console.log(b())

If I asked you what value console.log(b()) will log, it will be an obvious answer: 10.

It's 10 because function a() returns v, and v is defined above and has the value 10.

That's how static (also called lexical) scoping works. You can know the value just by looking at the code.

If dynamic scoping were what JavaScript uses, the logged value would be 20. It would be 20 because when running the code, the function b() would override the scope used for the variable v to the one defined in b(), and that would make a() use the last one, which is 20.

Thankfully, JavaScript doesn't use dynamic scoping and uses lexical scoping instead.

So you can simply think of lexical scoping as the scoping that allows us to know what variable will be used just by looking at the code at compile time (if the language is compiled) and not at runtime.

let and const vs. var

Advice everyone gets when learning JavaScript is to always use let and const instead of var. But why is that? Is var bad? Not necessarily.

It's all about scoping.

let and const are block-scoped, while var is function scoped. What does that mean?

It means that any variable declared with let or const will be available only within the block it was defined in. By block, I mean {} (curly braces).

This means this:

let a = 'foo'

{
  let b = 'bar'
  console.log(b) // Output: 'bar'
}

console.log(b) // Error: undefined

Look how I defined a block even without any additional keywords like if or for. That's a valid syntax, but in real-world code, the blocks are usually preceded with some keywords like when defining if statements, while loops, and functions.

This is why when we use let and const in if statements, for example, we can't use the variable outside of it.

if (true) {
  // Here is a new scope (inner scope)
  const a = 'foo'
}

// Here is the outer scope (another one)
console.log(a) // Error: undefined

So console.log logged undefined because it's in a different scope from the if statement. This means you can define the variable a again in the outer scope without affecting the one in the inner scope.

if (true) {
  const a = 'foo'
}

const a = 'bar'
console.log(a) // bar

So that's how let and const scoping works. Let's see how var works.

var is for defining function-scoped variables. This means it will create a new scope only if it is used within a function, like this:

function foo() {
  var a = 'foo'
}

console.log(a) // Error: undefined

Note that var is only function scoped (not block scoped), which means defining it within an if statement, for example, will define it globally (on the window object if in the browser).

if(true) {
  var a = 'foo'
}

console.log(a) // foo

So var will define a new scope if it's used in a function; otherwise, it will define the variable in the global scope.

Thus, we get the advice to prefer let and const over var because block-scoped variables are more predictable and easier to reason about.

Access direction

An important rule to remember in lexical scoping: inner scopes can access outer scopes, but not the other way around.

Here's an example:

let a = 1

// cannot access b or c

{
  // can access a
  // cannot access c
  
  let b = 2
  
  {
    // can access a
    // can access b
    
    let c = 3
  }
}

You can think of it as: you can't look inside things, but you can look outside them.

You need to always remember this rule as closures heavily depend on it as you will see next.

It also works for functions

Until now, we've been looking at examples that show how scoping works for variables. But the same is true for functions.

Let's take a look at this example:

function a() {
  function b() {
    console.log('from b')
  }
  
  b()
  
  console.log('from a')
}

b() // Error: b is not defined

It errors because the function b is not defined within the scope of the caller.

If we modify the example to call a() instead, it will work because a is defined within the scope of the caller.

function a() {
  function b() {
    console.log('from b')
  }
  
  b()
  
  console.log('from a')
}

a()
// Output:
// from b
// from a

Also, note how it was able to call the function b from function a because b is defined within the scope of a.

In this example, we have nested functions, so we are close to the concept of closures (get ready).

Returning a function from another function

Let's take the previous example and modify it to return the function b from the function a.

function a() {
  return function b() {
    console.log('from b')
  }
}

const nestedFunction = a()

nestedFunction() // Output: from b

Notice how we were able to overcome the restriction of scopes by returning the function. In the previous example, we weren't able to call the function b directly in the outer scope, but when we returned the function b, we were able to access it and call it.

Let's stop for a moment here to talk about the idea of returning a function.

When in a language a function can return another function (or take functions as arguments), we say two things:

  • this language supports functions as first-class citizens
  • and we call that function a Higher-order function (HoF).

This is why we say JavaScript supports functional programming.

One more step to create a closure

Let's take the previous example and make the function b use (return) a variable defined in the function a.

function a() {
  let v = 10
  return function b() {
    return v
  }
}

const nestedFunction = a()

console.log(nestedFunction()) // Output: 10

Believe it or not, you have created your first closure.

Here's the simplest definition of a closure:

A closure is a function that uses and retains access to a variable defined outside of its scope.

Two important things to note here: uses and retains access.

The word uses here means that the function can use the outside variables without the need to pass them as parameters.

And retains access means that the inner function will still have access to the outside variables even after the outer function finishes executing.

So in the example above, even after the function a() finished executing, the variable v was not removed by the garbage collector because the inner function b() might need to use it in the future.

A small note here: a function doesn't have to use outside variables to be considered a closure; it's still called a closure even if it's empty, but that's not a common thing we see.

Closures can also modify outer variables

Not only can closures access outer variables, but they can also modify them if they were defined with let.

function a() {
  let counter = 0
  return function b() {
    return counter++
  }
}

const nestedFunction = a()

console.log(nestedFunction()) // Output: 0
console.log(nestedFunction()) // Output: 1
console.log(nestedFunction()) // Output: 2

You can return multiple closures from a function

This is not a feature specific to closures, but it's a feature of JavaScript that is worth mentioning.

Let's modify the previous example to return two functions instead of one by returning an object.

function makeCounter() {
  let counter = 0
  return {
    inc() {
      return ++counter
    },

    getValue() {
      return counter
    }
  }
}

const counter = makeCounter()

console.log(counter.inc()) // Output: 1
console.log(counter.inc()) // Output: 2
console.log(counter.inc()) // Output: 3

console.log(counter.getValue()) // Output: 3

It's still the same idea, but this is how we usually return multiple closures.

Why closures?

Like anything in programming, there are many different ways to implement something. And closures are no different. While anything can be implemented without closures (for example, in languages that don't support closures), closures provide a much simpler and more intuitive way to do so.

The first case that comes to mind for closures in JavaScript is event handling. Let's take a look at this example:

let counter = 0

button.addEventListener('click', () => {
  counter++
  console.log(counter) // 1 .. 2 .. 3 ..
})

While this is a simple example, there are a few things to unpack:

We have two scopes here: the outer scope (where counter variable is defined), and the inner scope for the callback is defined in addEventListener.
Since the inner function has access to the outer scope, we consider it a closure.
This function gets called every time the user clicks the button.
Every time it's called, it increments the outer variable counter and logs it.

Look how easy event handling is because of closures. Because the closure maintains access to its outer scope, we were able to modify and log counter on each click.

This example shows us how closures are everywhere, and we might be using them without realizing it.

There are other cases where you want to create closures intentionally. And here are five different real-world examples of closures.

Conclusion

Closures are a fundamental part of JavaScript, and we are already using them every day without realizing it—just like in the event handling example.

Understanding them well helps us write code that's more reusable and flexible. In this article, I've shown you five different examples of how closures can be used, but there's no limit to what you can come up with.

Whenever you forget what closures are, just remember that they're functions that remember their surroundings.

Reference