Scope, Closures & Hoisting
Types of Scope
// 1. Global scope
var globalVar = "I'm global";
let globalLet = "Also global";
// 2. Function scope
function outer() {
var funcVar = "function scoped";
// funcVar accessible here
}
// funcVar NOT accessible here
// 3. Block scope (let/const only)
{
let blockLet = "block scoped";
const blockConst = "also block";
var blockVar = "NOT block scoped!"; // var ignores blocks!
}
// blockLet and blockConst NOT accessible here
// blockVar IS accessible here (var leaks!)
// 4. Module scope (when using ES modules)
// Variables are file-scoped, not global
Lexical Scope (Scope Chain)
Inner functions can access variables from outer functions.
const x = "global";
function outer() {
const x = "outer";
function inner() {
const x = "inner";
console.log(x); // "inner" (closest scope wins)
}
function innerNoX() {
console.log(x); // "outer" (finds in parent scope)
}
inner(); // "inner"
innerNoX(); // "outer"
}
console.log(x); // "global"
Hoisting
// var is hoisted with value undefined
console.log(a); // undefined (NOT ReferenceError!)
var a = 10;
console.log(a); // 10
// let and const are hoisted but in Temporal Dead Zone (TDZ)
console.log(b); // ReferenceError: Cannot access before initialization
let b = 20;
// Function declarations are fully hoisted
greet(); // works!
function greet() { console.log("Hello"); }
// Function expressions are NOT hoisted
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() { console.log("Hi"); };
Closures
A closure is a function that remembers variables from its creation scope even after that scope has finished executing.
function makeCounter() {
let count = 0; // "closed over" variable
return function() {
count++;
return count;
};
}
const counter1 = makeCounter();
const counter2 = makeCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
counter2(); // 1 (independent closure!)
Practical Closure Use Cases
// 1. Data privacy / encapsulation
function createBankAccount(initial) {
let balance = initial; // private!
return {
deposit(amount) { balance += amount; },
withdraw(amount) { balance -= amount; },
getBalance() { return balance; }
};
}
const account = createBankAccount(100);
account.deposit(50);
account.getBalance(); // 150
// account.balance — undefined, can't access directly!
// 2. Function factory
function makeMultiplier(factor) {
return (n) => n * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
double(5); // 10
triple(5); // 15
// 3. Memoization
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) return cache[n];
return cache[n] = fn(n);
};
}
Classic Closure Gotcha (var in loops)
// PROBLEM: All closures share the SAME `i` (var is function-scoped)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (not 0, 1, 2!)
// SOLUTION 1: Use let (block scope per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 ✅
// SOLUTION 2: IIFE to create new scope
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 100))(i);
}
// Logs: 0, 1, 2 ✅
Variable Shadowing
let name = "Global";
function greet() {
let name = "Local"; // shadows outer name
console.log(name); // "Local"
}
greet(); // "Local"
console.log(name); // "Global" (outer unchanged)
MCQ — Scope, Closures & Hoisting
Q1: What is a closure?
A) A function with no parameters B) A function that has access to variables from its outer scope even after that scope has returned ✅ C) A sealed/frozen object D) An immediately invoked function
Answer: B
Q2: What is the TDZ (Temporal Dead Zone)?
A) The time between declaring and assigning a var variable
B) A zone where let/const variables exist but cannot be accessed before their declaration line ✅
C) An area of memory reserved for temporary objects
D) The timeout period in async operations
Answer: B
Q3: What does var hoisting do?
A) Moves the declaration AND initialization to the top
B) Moves only the declaration to the top (initialized as undefined) ✅
C) Does nothing
D) Causes a ReferenceError
Answer: B
Q4: What is the output?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
A) 0, 1, 2
B) 3, 3, 3 ✅
C) undefined
D) 1, 2, 3
Answer: B — var is function-scoped, all closures share the same i which becomes 3.
Q5: How do you fix the above loop to log 0, 1, 2?
A) Change var to let ✅
B) Use async/await
C) Remove the arrow function
D) Use clearTimeout
Answer: A — let creates a new binding per iteration.
Q6: Can inner functions access outer function variables?
A) Only if passed as parameters B) Yes, through the scope chain (lexical scope) ✅ C) Only global variables
Answer: B
Q7: What is the output?
console.log(foo());
function foo() { return 42; }
A) undefined
B) ReferenceError
C) 42 ✅
D) TypeError
Answer: C — Function declarations are fully hoisted.