Functions, Scope and Closures · Lesson 10 of 40
JavaScript Scope and Closures
Closures are the concept that separates JavaScript beginners from confident developers – and they appear in almost every JavaScript interview. The idea is simple once you see it: a function remembers the variables from the place where it was created, even after that place has finished running. To understand that, we start with scope.
In this lesson
- Global, function and block scope
- The scope chain and lexical scoping
- What a closure is and why it works
- Real uses: private state, once(), memoisation
Scope: where a variable is visible
const appName = "LearnFrenzy"; // module (top-level) scope
function showCourse() {
const course = "JavaScript"; // function scope
if (true) {
const lesson = "Closures"; // block scope
console.log(appName, course, lesson); // inner code sees outer variables
}
console.log(typeof lesson); // block variable is gone
}
showCourse();
console.log(typeof course); // function variable not visible outside
LearnFrenzy JavaScript Closures undefined undefined
The scope chain
When code uses a variable, JavaScript looks in the current scope, then the enclosing scope, and so on outward until the global scope. If it's not found anywhere: ReferenceError.
Looking up lesson, course and appName from the innermost block
- Block scopelesson ✔ – found here
- showCourse() scopecourse ✔
- Module scopeappName ✔
- Global scopeconsole, Math, setTimeout… then ReferenceError
JavaScript uses lexical (static) scoping: what a function can see is decided by where it is written in the code, not where it is called from.
const level = "module";
function printLevel() { console.log(level); }
function run() {
const level = "inside run";
printLevel(); // looks where printLevel was written, not here
}
run();
module
Closures
A closure is a function together with the variables from its surrounding scope. When an outer function returns an inner function, the inner function keeps those variables alive:
function makeCounter() {
let count = 0; // private to this counter
return function () {
count++;
return count;
};
}
const nextTicket = makeCounter();
console.log(nextTicket(), nextTicket(), nextTicket());
const another = makeCounter(); // a fresh, independent count
console.log(another(), nextTicket());
1 2 3 1 4
What happens in memory
- makeCounter() runscreates a scope with count = 0
- returns the inner functionwhich holds a reference to that scope
- makeCounter has finishedbut its scope isn't garbage-collected – it's still referenced
- nextTicket() is calledreads and updates the same count each time
Each call to makeCounter creates a new scope, so another has its own count. Nothing outside can reach count directly – the closure makes it truly private.
Closures in real code
Private state with a public API
function createWallet(initial) {
let balance = initial;
const history = [];
return {
pay(amount) {
if (amount > balance) return "Insufficient balance";
balance -= amount;
history.push(-amount);
return `Paid ₹${amount}, left ₹${balance}`;
},
topUp(amount) { balance += amount; history.push(amount); },
get balance() { return balance; },
get history() { return [...history]; },
};
}
const wallet = createWallet(500);
console.log(wallet.pay(200));
console.log(wallet.pay(900));
wallet.topUp(100);
console.log(wallet.balance, wallet.history);
try {
wallet.balance = 1000000; // getter only – no way to set it
} catch (e) {
console.log(e.name + ":", e.message);
}
console.log(wallet.balance);
Paid ₹200, left ₹300 Insufficient balance 400 [ -200, 100 ] TypeError: Cannot set property balance of #<Object> which has only a getter 400
The only way to change balance is through pay and topUp, which enforce the rules. In strict mode (every ES module) writing to a getter-only property throws; in old non-module scripts it would be silently ignored.
once() and function factories
function once(fn) {
let done = false, result;
return (...args) => {
if (!done) { done = true; result = fn(...args); }
return result;
};
}
const initPayment = once(() => { console.log("Payment SDK loaded"); return "ready"; });
console.log(initPayment(), initPayment());
const multiplier = (factor) => (n) => n * factor; // returns a configured function
const double = multiplier(2), toPaise = multiplier(100);
console.log(double(21), toPaise(4.5));
Payment SDK loaded ready ready 42 450
Memoisation: caching results
function memoize(fn) {
const cache = new Map();
return (n) => {
if (cache.has(n)) return cache.get(n);
const value = fn(n);
cache.set(n, value);
return value;
};
}
let calls = 0;
const slowSquare = (n) => { calls++; return n * n; };
const fastSquare = memoize(slowSquare);
console.log(fastSquare(12), fastSquare(12), fastSquare(5));
console.log("real calls:", calls);
144 144 25 real calls: 2
The classic interview question
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("var", i), 0);
}
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log("let", i), 0);
}
var 4 var 4 var 4 let 1 let 2 let 3
Interview angle
All three var callbacks close over the same function-scoped i, which is 4 when the timers fire. let creates a new binding per iteration, so each callback closes over its own value. Before let existed, the fix was an IIFE: ((j) => setTimeout(() => console.log(j)))(i).
Watch memory
A closure keeps its whole scope alive. Attaching closures that capture large data to long-lived things (global event listeners, caches that never shrink) can leak memory – remove listeners you no longer need.
Practice
Try it yourself
Write
makeRateLimiter(limit)returning a function that returns true for the firstlimitcalls and false afterwards.Show solution
function makeRateLimiter(limit) { let used = 0; return () => { if (used >= limit) return false; used++; return true; }; } const canSendOtp = makeRateLimiter(2); console.log(canSendOtp(), canSendOtp(), canSendOtp());true true false
Quick check
Define a closure in one sentence.
A function bundled with references to the variables of the scope in which it was created, which it can still use after that scope has returned.
Is scope decided where a function is called or where it is written?
Where it is written – JavaScript is lexically scoped.
Two counters from makeCounter() – do they share count?
No – each call creates a new scope with its own count.
Summary
- Variables live in global, module, function or block scope; lookups walk the scope chain outward.
- Scope is lexical: decided by where code is written.
- Closures keep outer variables alive – use them for private state, factories, once() and caching.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.