Mastering closures in JavaScript
Tags: javascript, webdev, programming, beginners
Published: 2024-03-20 10:42:22

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.

```js
// 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 really 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:

```js
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, 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 literally means this:

```js
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 keyword like `if` or `for`. And 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.

```js
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.

```js
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 was used within a function, like this:

```js
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).

```js
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:

```js
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:

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

b() // Error: b is not defined
```

It errors because 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.

```js
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 function `b` from function `a` because `b` is defined within the scope of `a`.

In this example, we have nested functions, so we are really 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`.

```js
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 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).

Actually, this is why we say JavaScript supports functional programming.

## One more step to create a closure

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

```js
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`.

```js
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.

```js
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:

```js
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: outer scope (where `counter` is defined), and inner scope for the callback 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.

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

## Example 1: Encapsulation

Like in Object-oriented programming (OOP), we can achieve encapsulation through closures. Encapsulation essentially means hiding part of the data and the internal implementation.

In OOP, we can do this using the `private` keyword. In closures, we can simply do that by defining the data outside the closure, like this:

```js
function makeCounter() {
  // Private data
  let counter = 0

  // Private function
  function a() {}

  // Public
  return {
    inc() {
      counter++
    },

    dec() {
      counter--
    },

    getCounter() {
      return counter
    }
  }
}

const counter = makeCounter()

counter.inc()

console.log(counter.getCounter())
```

In this example, there's no way you can access `counter` or the function `a()` directly. You can only do that through its public API; in other words, the object returned from it.

## Example 2: Partial application

In mathematics, partial application means fixing a certain number of arguments in a function to create a new function with fewer arguments.

If that's a little bit confusing, don't worry. Here's a simpler explanation.

Let's say you have a function `f(a, b, c)`. This function takes three arguments: a, b, and c. Partial application means creating another function from function `f` where some arguments are hardcoded. For example, we can have a new function called `g(b, c)`, where `a` is always the value 10, for example.

We can do the same in JavaScript because functions are first-class citizens.

Let's take a look at this example:

```js
function add(a, b) {
  return a + b
}

function add10(b) {
	return add(10, b)
}

console.log(add10(5)) // Output: 15
```

In this example, we created another function called `add10` based on the original one `add`. The new function does the same but always uses the number 10 as the first argument.

This example is just for demonstration purposes, so it might not look useful. Let's take a look at a useful example that you might use in your project.

```js
function createFormatter(prefix, suffix) {
  return function (string) {
    return `${prefix}${string}${suffix}`
  }
}

const bracketsFormatter = createFormatter('[', ']')
const paragraphFormatter = createFormatter('<p>', '</p>')

console.log(bracketsFormatter('name')) // [name]
console.log(paragraphFormatter('some text')) // <p>some text</p>
```

You can see how we were able to create different types of formatters using the general one `createFormatter`. If I want to create another formatter that surrounds text with curly braces, I just need to do this:

```js
const curlyFormatter = createFormatter('{', '}')
```

So, with partial applications, we can make a certain function more reusable and flexible by fixing some of its arguments. All thanks to closures for enabling us to do that.

## Example 3: Validation functions

In this example, I want to create a way to validate values. If they are valid, don't do anything; otherwise, throw an error. With closures, we can do that easily.

```js
function createValidator(validatorFunction, errorMessage) {
  return function (value) {
    if (!validatorFunction(value)) {
      throw new Error(errorMessage)
    }
  }
}

const positiveValidator = createValidator((value) => {
  return value > 0
}, 'The provided number must be positive')

try {
  positiveValidator(-10) // will throw an error
} catch (error) {
  console.log('error', error)
}
```

`positiveValidator` is just one example. You can use `createValidator` to create any validator you want.

## Example 4: Throttling

Another good application of closures is throttling, where a function is called only once per some duration.

```js
function throttle(callback, duration) {
  let lastExecuted = 0

  return function (...args) {
    const now = Date.now()
    if (now - lastExecuted >= duration) {
      callback(...args)
      lastExecuted = now
    }
  }
}

const saveRequest = throttle((data) => {
  console.log('saving data', data)
}, 1000)

// Only one request is sent
saveRequest({ user: 'test' })
saveRequest({ user: 'test' })
```

As an exercise, see how you can modify the `throttle` function to be a `debounce` function.

## Example 5: Memoization

Memoization simply means caching the result of some expensive function.

In the following example, I'm writing a memoization function for addition, but in the real world, it will be something more expensive.

```js
function memoizedAdd() {
  const cache = {}

  function addFunc(a, b) {
    console.log('adding')
    return a + b
  }

  return function (a, b) {
    const key = `${a},${b}`
    if (cache[key]) {
      return cache[key]
    }
    const result = addFunc(a, b)
    cache[key] = result
    return result
  }
}

const add = memoizedAdd()

console.log(add(10, 5))
console.log(add(10, 5))
console.log(add(10, 1))

// Output:
// adding
// 15
// 15
// adding
// 11
```

You can see how the word `adding` was logged only once for adding 10 and 5, since the second time used the cached result. Imagine how beneficial this would be for expensive operations.

## Conclusion
Closures are a fundamental part of JavaScript, and we are already using them every day without realizing—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 basically functions that remember their surroundings.

---

### There are countless applications of closures. Are there any that haven't been mentioned in this article? Please share them in the comments section.

---

## 🔗 Let's stay connected:
- 🌐 Blog: https://tahazsh.com/
- 🎥 YouTube: https://www.youtube.com/@tahazsh
- 𝕏 Twitter/X: https://twitter.com/tahazsh