Understanding Closures in JavaScript?: A Front-End Developer’s Guide

Understanding Closures in JavaScript: A Front-End Developer’s Guide

As a front-end developer, I’ve recently realised how many times I’ve used closures in my React projects without giving them a second thought. While I am using closures everywhere, it wasn’t until I was having a conversation with a colleague that I realised I didn’t fully grasp what was happening under the hood. So I have just spent a couple of nights going down the rabbit hole of understanding exactly how closures work, and trust me, it was a game-changer…let’s explore closures together!??

What is a closure????

While they might seem mysterious at first, closures are fundamental to creating efficient, maintainable code — especially for building interactive UIs and managing state (think about how you use the useState hook in React — I'll provide an example at the end).

According to mdn web docs

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.

Let’s break this down with a small bank account example which helps me visualise a more “in real life” scenario: ??

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit: function (amount) {
      balance += amount;
      return `Deposited: ${amount}. New balance: ${balance}`;
    },
    withdraw: function (amount) {
      if (amount > balance) {
        return "Insufficient funds";
      }
      balance -= amount;
      return `Withdrew: ${amount}. New balance: ${balance}`;
    },
    getBalance: function () {
      return balance;
    },
  };
}
let account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50));  // Deposited: 50. New balance: 150
console.log(account.withdraw(70)); // Withdrew: 70. New balance: 80        

so, what exactly is happening here? well, I'm a big fan of seeing things or having them sketched out to help me understand it, so, let’s have a visual aid and then get into it!??

Each method, maintains access to the balance variable through closure, meaning that even when the outer function has finished executing, we still get access to the variable balance that was inside it! ??

How Closures Work in Our Example ??

Let’s get into the nitty gritty of what’s happening:

  1. We define createBankAccount() (typically referred to as the parent or outer function) which takes an initial balance and sets up a local variable balance.
  2. Inside this function, we define three other functions: deposit(), withdraw(), and getBalance(), typically referred to as the inner function(s).
  3. We return an object containing these three functions.
  4. When we call createBankAccount(100), it returns this object, which we store in account.
  5. Now comes the magic ??♂.?.. even though createBankAccount() has finished executing, when we call account.deposit(50), it still has access to the balance variable that was defined in its parent function!

This is a closure in action. The inner functions “close over” or “capture” if you will, the variables in their parent scope, maintaining access to them.

Why Closures Matter for Front-End Development

As front-end developers, we use closures all the time, often without realizing it:

1. Private Variables

In our bank account example, the balance variable is private - it can't be accessed directly from outside, only through the provided methods. This is a form of encapsulation that helps prevent bugs.

So, could we console log out balance? nope!

// This won't work - balance is not accessible!
console.log(account.balance); // undefined

// We must use the getter method
console.log(account.getBalance()); // 80        

2. Factory Functions

Our createBankAccount is a factory function (function that returns a function) that can create multiple independent accounts, each with their own private balance:

let account1 = createBankAccount(100);
let account2 = createBankAccount(500);

account1.deposit(50);  // balance becomes 150
account2.withdraw(100); // balance becomes 400

console.log(account1.getBalance()); // 150
console.log(account2.getBalance()); // 400        

Each account has its own independent balance variable in its closure!

It’s important to note that once createBankAccount has executed, we don’t actually execute that again, what we access after this is the result of running createBankAccount which for us, means the methods and the state variables at time of executing. Closures not only maintain access to variables but also maintain their own separate execution context for each instance, which is why account1 and account2 have independent balances.

3. Event Handlers

Closures shine when working with event handlers and callbacks:

function setupCounter() {
  let count = 0;
  
  document.getElementById('increment').addEventListener('click', function() {
    count++;
    document.getElementById('counter').innerText = count;
  });
  
  document.getElementById('decrement').addEventListener('click', function() {
    count--;
    document.getElementById('counter').innerText = count;
  });
}

setupCounter();        

Each event handler maintains access to the same count variable through closure, even after setupCounter() has finished executing.

A closure pitfall, using SetTimeout!

Hey, we’ve done well to get through it so far..let’s look at a common pitfall that can catch even the most seasoned developers out, and for this I am going to use an example that includes an asynchronous setTimeout .

Let’s peek at the code first ??

function loop() {
  let i = 0;

  for (i; i < 10; i++) {
    setTimeout(() => {
      console.log(i); // What is 'i' going to be ??
    }, 100);
  }
}

loop();
// Output: 10,10,10,10,10,10,10,10,10        

Ah, so we just got 10 printed out ten time, that’s not ideal! The reason i is printing out 10ten times is that setTimeout is an asynchronous function. When the loop runs, it schedules the setTimeout callbacks to execute after 100 milliseconds, but by the time those callbacks are executed, the loop has already completed all its iterations. At that point, i has been incremented to 10. Since setTimeout references i, it logs the current value of i, which is now 10. This is not what we intended; we wanted to print the value of i at each iteration. Let’s fix this!

function loop() {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      console.log(i); // This ought to be right now, right?!
    }, 100);
  }
}

loop();

// Output 0,1,2,3,4,5,6,7,8,9 

??????        

Excellent! our function output exactly what we intended it to, and the reason for that is that each iteration created a new binding for i preserving the value for the setTimeout callback to grab when it was ready.

Closures in React

This has really highlighted to me just how frequently I take closures for granted when building out react apps! I’ve put together a small example to show this.

import React, { useState } from 'react';

// Counter here is our outer function
const Counter = () => {
  const [count, setCount] = useState(0);

// incrementCount is our inner function
  const incrementCount = () => {
    // Using a closure to access the current state value
    // This ensures we are working with the latest count value,
    // even if multiple updates occur quickly.
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
};

export default Counter;        

Lets run through this last example!

  1. We initialize the state variable count with useState(0), setting the initial count to 0.
  2. The incrementCount function is defined to update the count . Instead of directly using setCount(count+1) , we use a function inside setCount : *setCount(prevCount => prevCount + 1).
  3. The function passed to setCount is a closure that captures the current value of prevCount. This ensures that even if multiple clicks happen in quick succession, each call to setCount will correctly reference the most recent state value.
  4. When the button is clicked, the incrementCount function is called, which updates the state. The component re-renders, displaying the updated count.

* setCount here is itself a closure as it closes over the most recent state value, making it safter for multiple state updates rather than using setCount(count + 1).

Conclusion ??

Closures aren’t just an academic concept — they’re a practical tool that front-end developers use every day. In React, for instance, we might build a feature where we want a function to be triggered only once; in this case, we could use a closure with a boolean switch to control execution. Alternatively, we might implement a visitor count incrementor for site metrics, where a closure allows us to maintain and update the count each time a visitor interacts with the site.

The next time you’re writing JavaScript, pay attention to where you’re using closures — you might be surprised by how often they appear in your code and are helping you out! ??



Liam Anderson

Fullstack Software Developer

1 个月

A fantastic way of explaining a core attribute behind the power of functional programming! There is some wizardry programming available through this however which can be fun to write but hard to read, so use it carefully and simply. Your examples are an excellent introduction to this!

David Hay

Client Services Manager at AND Digital

1 个月

Nice one matey! ?? didn’t understand a word of it, but was delighted to see it was penned by your good self!

要查看或添加评论,请登录

ClearSky Logic的更多文章

社区洞察

其他会员也浏览了