The Secret Life of JavaScript: Understanding Closures
Source: Dev.to
Functions Remember
Timothy arrived at the library soaking wet, rain dripping from his jacket. He’d been running through London’s streets, desperate to talk to Margaret about something that had broken his understanding of how functions work.
“It’s impossible,” he said, shaking water from his hair. “I wrote code that violates everything I learned about memory and scope.”
Margaret looked up from her tea.
“Show me.”
Timothy pulled out his laptop and typed:
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
“I don’t understand,” Timothy said. “When
createCounterfinishes running, its local variables should be garbage collected. Thecountvariable should be gone. But the returned function still knows about it. It’s accessing a variable that shouldn’t exist anymore.”
Margaret smiled.
“You’ve discovered closures. And you’re right to be confused—it looks like a violation of how programming languages work. Except in JavaScript, it’s not a bug. It’s a feature.”
Lexical Scope
Margaret pulled out her notebook.
“Before we explain what’s happening, we need to understand lexical scope. You know scope from Chapter 1—where variables live. But there’s something deeper.”
She wrote:
function outer() {
let outerVar = "I'm in outer";
function inner() {
console.log(outerVar); // Can I see this?
}
inner();
}
outer(); // "I'm in outer"
“When
innerlooks forouterVar, where does it look?” Margaret asked.
“It looks where the variable is declared,” Timothy answered. “In the outer function.”
“Exactly. It doesn’t matter where
inneris called. It matters whereinnerwas written. This is lexical scope: functions look for variables in the scope where they were defined, not where they are executed.”
Returning an Inner Function
function outer() {
let outerVar = "I'm in outer";
function inner() {
console.log(outerVar);
}
return inner; // Return the function itself
}
const myFunction = outer(); // `outer()` finishes here
myFunction(); // "I'm in outer" - but how?!
“When
outerfinishes executing, its local variables should disappear,” Timothy said slowly. “Butinneris still accessingouterVar. How is that variable still alive?”
“Because JavaScript doesn’t let it die,” Margaret replied. “When you return
inner, JavaScript says: ‘This function referencesouterVar. I can’t garbage collect that variable yet, because the function might be called later and it will need that variable.’ So JavaScript keepsouterVarin memory specifically forinnerto use.”
“So the variable persists?”
“The variable persists. The function remembers its birthplace. This combination—a function plus the variables it needs from its scope—is called a closure.”
The Backpack Analogy
Margaret leaned back.
“Imagine every function gets a backpack when it’s created. As the function is being defined, JavaScript looks around and says: ‘What variables will this function need?’ Then it packs them into the backpack.”
function createGreeter(greeting) {
// createGreeter's scope
return function(name) {
// This inner function needs `greeting` from createGreeter's scope
console.log(greeting + ", " + name);
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
sayHello("Alice"); // "Hello, Alice"
sayHi("Bob"); // "Hi, Bob"
“When you define the inner function, it looks around. It sees
greeting. It packsgreetinginto its backpack. Later, no matter where that function is called, it carriesgreetingwith it.”
Timothy pointed at the code.
“So
sayHelloandsayHihave different backpacks? Differentgreetingvalues?”
“Exactly. Each time
createGreeterruns, a new scope is created, a newgreetingvariable is created, and the returned function gets that specificgreetingin its backpack. They are completely independent.”
Closures for Data Privacy
function createAccount() {
let balance = 100;
return {
deposit: function(amount) {
balance += amount;
console.log("Balance: " + balance);
},
withdraw: function(amount) {
balance -= amount;
console.log("Balance: " + balance);
},
getBalance: function() {
return balance;
}
};
}
const account = createAccount();
account.deposit(50); // Balance: 150
account.withdraw(20); // Balance: 130
account.getBalance(); // 130
“All three methods are closures over
balance. They all see the samebalancevalue, right?”
“Yes. And crucially, it’s a live reference, not a copy.”
Further demonstration:
account.deposit(50); // Balance: 150
account.withdraw(20); // Balance: 130
account.deposit(100); // Balance: 230
// All three methods access the SAME variable.
// If one changes it, all others see the change.
Attempting to access the private variable directly fails:
console.log(account.balance); // undefined
account.balance = 9999; // Creates a new property; does not affect the closure variable
console.log(account.getBalance()); // Still 230
“The
balancevariable is completely hidden. The only way to interact with it is through the three methods you exposed. This is genuine data privacy.”
The Module Pattern
const calculator = (function() {
let lastResult = 0;
return {
add: function(a, b) {
lastResult = a + b;
return lastResult;
},
subtract: function(a, b) {
lastResult = a - b;
return lastResult;
},
getLastResult: function() {
return lastResult;
}
};
})();
calculator.add(10, 5); // 15
calculator.subtract(20, 8); // 12
calculator.getLastResult(); // 12
// `lastResult` is private—no one can access or modify it except through these methods.
“This is a module. You use an immediately‑invoked function expression (IIFE) to create a private scope, then return an object with public methods that access those private variables. Before ES6 classes existed, the Module Pattern was the primary way to create objects with private state in JavaScript.”
A Classic Closure Pitfall
function setupButtons() {
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log("Button " + i + " clicked");
}, 1000);
}
}
“This is the
varproblem from Chapter 1, mixed with closures. Withvar, the variableibelongs to the entiresetupButtonsfunction, not to the loop block. All three callbacks share the samei, which after the loop ends has the value4(the loop exits whenibecomes4).”
Fixes: use let for block scoping or create a new scope for each iteration.
// Using let (ES6)
function setupButtons() {
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log("Button " + i + " clicked");
}, 1000);
}
}
// Using an IIFE
function setupButtons() {
for (var i = 1; i <= 3; i++) {
(function(index) {
setTimeout(function() {
console.log("Button " + index + " clicked");
}, 1000);
})(i);
}
}