Day 16: Scope, Hoisting & Closures
✅ Today you will learn
- Global, function and block scope; the scope chain
- Hoisting: var vs let/const vs function declarations, the TDZ
- Closures: functions that remember
- Real uses: private state, factories, memoise, once
- The famous var-in-a-loop bug
🏢 In the real world
“Explain closures” is one of the most asked JavaScript interview questions in India, from service companies to product startups. It is also practical: every event handler, timer and React hook relies on closures. Today you will understand them well enough to explain with a sweet-shop example – and fix the bug that has confused developers for 20 years.
Scope and the scope chain
const shop = 'Sharma Sweets'; // global scope
function makeInvoice(orderNo) { // function scope
const prefix = 'INV';
if (orderNo > 1000) {
const series = 'B'; // block scope
return `${shop} ${prefix}-${series}${orderNo}`; // looks outward: block → function → global
}
return `${shop} ${prefix}-${orderNo}`;
}
console.log(makeInvoice(42));
console.log(makeInvoice(1042));
console.log(typeof prefix, typeof series); // not visible outsideHoisting and the temporal dead zone
console.log(greet('Riya')); // ✅ function declarations are hoisted with their body
function greet(n) { return 'Namaste ' + n; }
console.log(oldStyle); // undefined – var is hoisted but set to undefined
var oldStyle = 'var';
try {
console.log(modern); // ❌ let/const exist but are in the "temporal dead zone"
let modern = 'let';
} catch (err) {
console.log(err.name + ': ' + err.message);
}| Declaration | Hoisted? | Before the line |
|---|---|---|
| function name() {} | Yes, with body | Can be called |
| var x | Yes, as undefined | undefined (silent bugs) |
| let / const | Yes, but uninitialised (TDZ) | ReferenceError |
| const f = () => {} | Like const | ReferenceError |
| class A {} | Like let (TDZ) | ReferenceError |
Closures: functions that remember
function createCounter(label) {
let count = 0; // private – nobody outside can touch it
return function () { // this inner function "closes over" count and label
count++;
return `${label}: token #${count}`;
};
}
const counterA = createCounter('Counter A');
const counterB = createCounter('Counter B');
console.log(counterA());
console.log(counterA());
console.log(counterB()); // separate memory for each counter
console.log(counterA());
console.log(typeof count);A closure is a function bundled with the variables of the place where it was created. Even after createCounter has finished, the returned function still has access to its own count – like a shop token machine that keeps its own number.
Real uses of closures
// 1. Private state with a public API (module pattern)
function createWallet(start) {
let balance = start;
return {
pay(amount) {
if (amount > balance) return `Insufficient balance (₹${balance})`;
balance -= amount;
return `Paid ₹${amount}, left ₹${balance}`;
},
get balance() { return balance; },
};
}
const wallet = createWallet(1000);
console.log(wallet.pay(650));
console.log(wallet.pay(500));
wallet.balance = 999999; // no setter → cannot be changed from outside
console.log(wallet.balance);
// 2. Function factories
const makeDiscount = percent => price => Math.round(price * (1 - percent / 100));
const diwali10 = makeDiscount(10), member5 = makeDiscount(5);
console.log(diwali10(900), member5(900));
// 3. once() – e.g. a coupon that can be applied only once
function once(fn) {
let done = false, result;
return (...args) => (done ? result : ((done = true), (result = fn(...args))));
}
const applyCoupon = once(code => `Coupon ${code} applied`);
console.log(applyCoupon('SWEET10'), '|', applyCoupon('AGAIN'));
// 4. Memoisation – cache slow results
function memo(fn) {
const cache = new Map();
return n => (cache.has(n) ? cache.get(n) + ' (cached)' : (cache.set(n, fn(n)), cache.get(n)));
}
const slowGst = memo(amount => amount * 0.05);
console.log(slowGst(2000), slowGst(2000));The classic var-in-a-loop bug
const withVar = [], withLet = []; for (var i = 1; i <= 3; i++) withVar.push(() => i); // ONE shared i for all functions for (let j = 1; j <= 3; j++) withLet.push(() => j); // a NEW j for every round console.log(withVar.map(f => f())); // [4, 4, 4] ❌ console.log(withLet.map(f => f())); // [1, 2, 3] ✅
var is function-scoped, so all three closures remember the same variable, which is 4 after the loop ends. let creates a fresh binding for each iteration. This is the #1 reason modern code uses let/const.
Real task: independent token counters
<div id="counters"></div>
<script>
function createTokenCounter(name) {
let next = 1; // private per counter
const box = document.createElement('div');
box.style.cssText = 'display:inline-block;margin:6px;padding:12px 16px;border:1px solid #e5e7eb;border-radius:12px;text-align:center';
const title = document.createElement('b');
title.textContent = name;
const shown = document.createElement('div');
shown.style.fontSize = '2rem';
shown.textContent = '—';
const btn = document.createElement('button');
btn.textContent = 'Next token';
btn.addEventListener('click', () => { shown.textContent = 'A' + String(next++).padStart(3, '0'); }); // closure!
box.append(title, shown, btn);
return box;
}
document.querySelector('#counters').append(
createTokenCounter('Sweets counter'),
createTokenCounter('Namkeen counter'),
createTokenCounter('Billing'));
</script>🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
function createRateLimiter(limit) {
let calls = 0;
return function (action) {
calls++;
return calls <= limit ? `OK: ${action} (${calls}/${limit})` : 'Too many requests';
};
}
const sendOtp = createRateLimiter(3);
['otp', 'otp', 'otp', 'otp'].forEach(a => console.log(sendOtp(a)));Show solution
let x = 'global';
function outer() {
let x = 'outer';
function inner() { return x; }
return inner;
}
const f = outer();
x = 'changed global';
console.log(f()); // 'outer' – inner remembers outer's x, not the global oneQuick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
What is a closure? Give a practical example.
A closure is the combination of a function and its lexical environment, so the function keeps access to outer variables after the outer function returns. Example: a counter/rate-limiter with private state, event handlers that remember their item, or a memoised function with a private cache.
Explain hoisting.
During compilation declarations are registered in their scope: function declarations fully, var as undefined, let/const/class as uninitialised (temporal dead zone) – so they exist but cannot be read before the declaration line.
Summary
- Variables are looked up from block → function → global.
- Functions hoist fully; var as undefined; let/const are in the TDZ.
- Closures = functions + remembered variables → private state, factories, once, memo.
- Use let in loops to avoid the shared-var bug.
Day 17: this, classes and object-oriented JavaScript – build a reusable Cart class.
