JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
31: Closures Explained
I want to show you a piece of code that looks perfectly logical at first glance, but behaves in a way that has driven countless developers crazy. Imagine you're building a simple menu where clicking a button tells you which item index was selected.
const buttons = document.querySelectorAll('.menu-item');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('You clicked item number: ' + i);
});
}
You'd expect that clicking the first button prints "0", the second "1", and so on. But here's the problem: no matter which button you click, it always prints the total number of buttons (e.g., "3"). It feels like the code is lying to you.
The shared reference trap
The reason this happens is that the function inside the event listener isn't "saving" the value of i at the moment the listener was created. Instead, it holds a reference to the variable i itself. Because we used var, i is function-scoped (or global), meaning there is only one i for the entire loop.
By the time you actually click a button, the loop has already finished running. The single i variable is now sitting at 3. Every single click handler looks at that same variable and sees the same final number. This is the "aha!" moment where closures become essential.
Creating a snapshot with a closure
To fix this, we need each click handler to have its own private copy of i. We do this by wrapping the listener in another function. This inner function creates a "closure"βit captures the value of i at that specific moment in time and holds onto it.
const buttons = document.querySelectorAll('.menu-item');
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[i].addEventListener('click', function() {
console.log('You clicked item number: ' + index);
});
})(i);
}
By using an Immediately Invoked Function Expression (IIFE), I'm passing the current value of i into a new function. The index variable is local to that inner function. Now, each button gets its own unique index variable that doesn't change when the loop continues. I'll be honest: in modern JS, using let in the loop header solves this because let creates a new binding for every iteration, but understanding the closure mechanism is what actually makes you a senior engineer.
The power of private state
Closures aren't just for fixing loop bugs. Their real power is "data hiding." In JavaScript, we don't have a private keyword for variables inside functions. Closures give us a way to mimic this. If you return a function from another function, the returned function still has access to the parent's variables, but the rest of the world does not.
Check this out:
function createScoreTracker() {
let score = 0; // This variable is "private"
return {
increment: function() {
score++;
return score;
},
decrement: function() {
score--;
return score;
},
getScore: function() {
return score;
}
};
}
const player1 = createScoreTracker();
console.log(player1.increment()); // 1
console.log(player1.increment()); // 2
console.log(player1.score); // undefined! The variable is protected.
I love this pattern because it prevents other parts of your application from accidentally overwriting score = "pizza"` or some other nonsense. You've created a controlled interface to a piece of state that exists only within that closure.
π Practical Task
Build a Private-State Digital Stopwatch
Your task is to create a stopwatch factory function called createStopwatch. This function should use closures to protect the stopwatch's internal state so it cannot be modified from the outside.
Requirements:
- The
createStopwatchfunction should initialize astartTimeand arunningboolean as private variables. - It must return an object with three methods:
start(): Sets thestartTimeto the current time and setsrunningto true.stop(): Setsrunningto false.getTime(): Returns the elapsed time in seconds. If the watch isn't running, it should return the time elapsed since the laststop()call.
- Ensure that the
startTimeandrunningvariables are not accessible directly from the returned object (e.g.,myWatch.startTimeshould beundefined).
There are no comments for now.