Closures, Closures, Closures !!!!๐ค
If you've ever felt puzzled by closures in JavaScript, you're in good company. You could be using closures every day in your code and not even know it! Yep, true story. Believe it or not, I once aced a closure-based interview question without fully understanding what closures were. However, understanding closures can significantly elevate your coding skills.
#What is Closure in Javascript ?
MDN says "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 you access to an outer function's scope from an inner function."
Maybe we still can't define what closure is yet. We can at least agree that it is enclosed in some form of container called the Lexical environment, it also has something to do with functions bundled together with something about references.
What then is a lexical environment?
Clearly, we have to understand some basic concepts first before delving into closure itself as a concept.
Concepts around closure
Concept #1: Scoping / Lexical scoping.
Scoping simply means the locations a declared variable has "access" to and the lexical behind it tells you that the scope of a variable is determined based on its location in the source code.
In JavaScript, especially modern javascript, where you declare your variables matter. like, a lot! Variables defined in a function are not accessible outside of it, but they are accessible to functions nested/ inside that function.
const bookName = "readings";
function getAuthorName(){
const author = "Ajayi";
console.log(author, bookName); //"Ajayi readings"
function nestedFunction(){
//has access to variable declared in the parent function
// also has acess to global variables. E.G bookName
}
return author;
}
console.log(bookName) //"readings"
console.log(author) //Uncaught ReferenceError: author is not defined
As explained earlier, where you declare your variables matter in JavaScript because the scopes of these variables are created based on their location in the source code.
The rule is simple.
Variables defined with "let" or "const" within a block of code are only accessible within that block.
Hence, it is easy to understand why author
and bookname
in the example above are accessible within the function getAuthorName
but trying to access author
outside of the function throws an error.
Concept #2: Block Scoping, Functional Scoping, Global Scoping, and the Story of const
,let
and var.
This is a topic that deserves its own stage, but let's do a quick fly-by.
Block Scoping: Variables declared with
let
andconst
stick to their block. It's just the modern way of doing things.Functional Scoping:
var
is the OG here and it lives within the function it's declared in. It transcends the block scoping.Global Scoping: Variables in the global scope are not declared within any block or functions, they can be accessed anywhere in the code.
const
vslet
vsvar
: Useconst
for constants,let
for reassignable variables, andvar
... well, mostly avoid. ๐คทโโ๏ธ
For a deep dive into this, check out this article that explains it all. Don't worry, I'll tackle this head-on in a future article!
Concept #3: Lexical environment
This is the tricky bit and it is quite theoretical in its own right. let me try to explain with a simple analogy.
Imagine you're a detective, and you're trying to find clues to solve a case. The place you'd look for these clues first is where the incident has happened (the crime scene), then you might find clues that lead you to look outside of that immediate environment. somewhere outside of the scene. The scene is the lexical environment. In JavaScript, a Lexical Environment exists for each block of code, each function, and yes, even for the global context that runs. Each lexical environment has two parts:
Environment Record: This is the abstract location in which something has happened in code. E.G. a function call. It stores all the variables and declarations in that function.
Reference to Outer Environment: Don't you love it when you're not confined within your space? same for our environment too. The environment likes to keep a reference to the Lexical Environment of the parent scope, parent parent scope, or global scope. You get the idea. It keeps a reference to the outer environments it will have access to.
Here's a quick example to quench your cravings for some lines of code.
let city = "Gotham";
function batSignal() {
let message = "Bat Signal activated in " + city;
function displaySignal() {
console.log(message); // "Bat Signal activated in Gotham"
}
displaySignal();
}
batSignal();
In this example, the function displaySignal()
is nested inside batSignal()
making it a Lexical Environment within another Lexical Environment - remember every function has its own Lexical Environment? When displaySignal()
wants to print message
, it first checks its own Environment Record (nope, not here!), and then the Reference to the Outer Environment kicks in, leading it to find message
in batSignal()
's environment, a parent environment. ๐ฆ Kapish!!!
Perhaps now, MDN's definition will start to make sense. A closure, being a combination of a function with reference to.....
Concept #4: The Beast; Closures.
Remember when we talked about Lexical Environments? Good, because closures are like little minions that carry their own Lexical Environment wherever they go. Let me spill the tea.
A closure is a JavaScript function that remembers its outer variables even after the outer function has "returned" or after it has been "dissipated". In simpler terms, closures are functions that "remember" the environment in which they were created. ๐ค
Let's take our next illustration from Leetcode. The createCounter question.
//Given an integer n, return a counter function. This counter function
//initially returns n and then returns 1 more than the previous
//value every subsequent time it is called (n, n + 1, n + 2, etc).
function createCounter(n) {
return function() {
return n++;
};
}
// Example usage
const counter = createCounter(10);
console.log(counter()); // 10
console.log(counter()); // 11
console.log(counter()); // 12
Here, counter
is a closure because every time you call it, it still knows the latest value of n and still has the ability to update it
How It Works ๐งโโ๏ธ
createCounter(n)
returns a function (anonymous function). That function remembers the state ofn
even aftercreateCounter
has finished its execution. why? reference to outer lexical environments.Each time the returned function
counter
is called, it returns the current value ofn
and then increments it by 1 using the post-increment operator (n++
)
and that my friends, is how closures work. The returned function, which is the closure is like a minion that goes about with its own suitcase containing its own lexical environment and references to outer environments that scoping has allowed it access to.
The Nitty-Gritty: Specific Use-Cases for Closures
Use-Case #1: The Vault
JavaScript objects are wide open by default and can be manipulated at will. but you don't always want that. Sometimes, you want to create a vault that not everyone has access to and then keep your secret variables there. You see, before the introduction of classes and private variables in Javascript, Closures were a way of keeping variables private within a function so that only methods within that function have access to retrieving or changing their value.
function createBankAccount(initialDeposit) {
let balance = initialDeposit;
return {
deposit: function(amount) {
balance += amount;
console.log(`Deposited ${amount}. Current balance: ${balance}`);
},
withdraw: function(amount) {
if (amount > balance) {
console.log('Insufficient funds');
} else {
balance -= amount;
console.log(`Withdrew ${amount}. Current balance: ${balance}`);
}
}
};
}
const myAccount = createBankAccount(1000);
myAccount.deposit(200); // You can do this!
myAccount.withdraw(500); // And this!
console.log(myAccount.balance); // But not this, Error!
In the code above, balance
is a private variable that can't be accessed directly from outside the createBankAccount
function. It can only be modified using the deposit
and withdraw
methods. why? aha! You should already know. because deposit
and withdraw
are closures. They are the only ones who still remember the value of balance even after the function createBankAccount
has been executed and forgotten about.
Use-Case #2: Time Traveller; the secure timekeeper.
function launchSequence(start) {
let count = start;
return function() {
console.log("T-minus " + count);
count--;
}
}
const countdown = launchSequence(10);
for (let i = 0; i <= 10; i++) {
setTimeout(() => {
countdown();
}, i * 1000);
}
You can use closures to create timekeeping functions that cannot be tampered with from the outside world.
Here, countdown
is a closure. It's carrying count
from launchSequence()
with it. Every time you call countdown()
, it still knows what count
is and updates it. countdown()
carries its own supply of reference to count
in its little briefcase
use-case #3: The Function Industry. We make food and we make functions.
Imagine you're operating a food truck. You've got your menu, but you also allow customers to customize their sandwiches. You need a way to easily produce variations of your menu items.
function sandwichMaker(fillings) {
return function(breadType) {
return `Here's your ${fillings} sandwich on ${breadType} bread!`;
}
}
const makeChickenSandwich = sandwichMaker("chicken");
const makeVeggieSandwich = sandwichMaker("veggie");
console.log(makeChickenSandwich("sourdough")); // "Here's your chicken sandwich on sourdough bread!"
console.log(makeVeggieSandwich("rye")); // "Here's your veggie sandwich on rye bread!"
here, sandwichMaker
is a function that takes a filling (like "chicken" or "veggie") and returns another function. The returned function then takes a type of bread, completes the sandwich, and serves it up!
So, every time a customer comes, you just have to call your pre-configured makeChickenSandwich
or makeVeggieSandwich
function with their choice of bread. Each function can be attached to its custom button. Now, you only have to hit the chicken or veggie button without having to worry about what could have been if you had only one sandwich button.
Use-Case #4: left Hook, right Hook, southpaw, useState Hook.
This is easily my favorite use case. I have been using the React's useState
hook for years. I never cared about its implementation details. still don't.
however, you might begin to wonder, perhaps, the useState
hook uses closure under the hood.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
What is going on here?
State Preservation:
useState
gives you a variable (state
) and a function to update it (setState
). This state variable is preserved between function calls, much like a closure!Functional Updates: This is where closures really kick in. When you update the state based on the previous state, you do something like this:
setCounter(prevCounter => prevCounter + 1);
Notice
prevCounter
? That's the previous state captured and passed into the function. setCounter must be a closure, folks!
Closing Prayer
Understanding closures in JavaScript is crucial for anyone looking to master the language. Closures offer a way to preserve state, encapsulate private variables, and enable dynamic function generation. Even if you're not keeping closures in your pocket for everyday use, knowing how they work demystifies a whole bunch of JavaScript quirks. Think of it as being able to see The Matrix. This understanding will prove invaluable across various JavaScript frameworks and libraries.