Scope, Hoisting & Closure
Checking access...
Scope determines where variables are accessible in your code. JavaScript has three scope levels and a special hoisting behaviour that moves declarations to the top of their scope before execution.
Global Scope
Variables declared outside any function or block belong to the global scope. They are accessible everywhere in your program.
// global scopeconst appName = "Weather App";
function showName() { console.log(appName); // "Weather App" — accessible inside functions}
showName();console.log(appName); // "Weather App" — accessible globallyIn browsers, global variables become properties of the window object. In Node.js, they belong to the global object.
Caution
Global variables are convenient but dangerous. Any script on the page can read or overwrite them. Use global scope only for values that are truly application-wide (config objects, constant enums).
Function Scope
Variables declared with var inside a function are scoped to that function — they cannot be accessed outside it.
function greet() { var message = "Hello!"; console.log(message); // "Hello!"}
greet();console.log(message); // ❌ ReferenceError: message is not definedThis creates a safety boundary — function internals don’t leak to the outside world.
Block Scope (let / const)
let and const are scoped to the nearest block — any code between { }.
if (true) { let blockScoped = "I'm trapped"; var functionScoped = "I escape!";}
console.log(functionScoped); // "I escape!"console.log(blockScoped); // ❌ ReferenceErrorThe same applies to for loops — using let creates a new binding for each iteration:
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0, 1, 2}
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 3, 3, 3}Hoisting
JavaScript’s engine hoists (moves) declarations to the top of their scope during compilation. The behaviour differs by keyword:
| Declaration | Hoisted? | Initialized? | Temporal Dead Zone? |
|---|---|---|---|
function | ✅ Yes | ✅ Yes (with definition) | ❌ No |
var | ✅ Yes | ✅ Yes (undefined) | ❌ No |
let | ✅ Yes | ❌ No | ✅ Yes |
const | ✅ Yes | ❌ No | ✅ Yes |
Function Hoisting
Function declarations are hoisted entirely — you can call them before they appear in code:
sayHello(); // "Hi there!" — works because of hoisting
function sayHello() { console.log("Hi there!");}var Hoisting
var declarations are hoisted but initialized to undefined:
console.log(age); // undefined (not an error!)var age = 25;This is why var is considered confusing — the code reads as if age exists before its assignment.
let / const Hoisting & TDZ
let and const are hoisted but not initialised. Accessing them before their declaration throws a ReferenceError because of the Temporal Dead Zone (TDZ):
console.log(city); // ❌ ReferenceError: Cannot access 'city' before initializationlet city = "London";The TDZ lasts from the start of the block until the declaration is encountered:
{ // TDZ starts here for 'name' const fn = () => console.log(name); // still TDZ let name = "Alice"; // TDZ ends here fn(); // "Alice"}Closures
A closure is a function that “remembers” the variables from its lexical scope even after that scope has exited.
function createCounter() { let count = 0;
return function() { count++; return count; };}
const counter = createCounter();console.log(counter()); // 1console.log(counter()); // 2console.log(counter()); // 3The inner function closes over the count variable. Even though createCounter has finished executing, the returned function keeps a reference to count in its closure scope.
How Closures Work
Every function has a hidden property called [[Environment]] that stores a reference to the lexical environment where the function was created. When the function is called, this environment is used as the outer scope reference, forming a scope chain:
- Local scope (the function’s own variables)
- Closure scope (outer function’s variables)
- Module/global scope
Practical Closure Patterns
1. Data Privacy (Factory Functions)
function createBankAccount(initialBalance) { let balance = initialBalance;
return { deposit(amount) { balance += amount; return balance; }, withdraw(amount) { if (amount > balance) return "Insufficient funds"; balance -= amount; return balance; }, getBalance() { return balance; } };}
const account = createBankAccount(1000);account.deposit(500);console.log(account.getBalance()); // 1500console.log(account.balance); // undefined — truly private2. Partial Application / Currying
function multiply(a) { return function(b) { return a * b; };}
const double = multiply(2);const triple = multiply(3);
console.log(double(5)); // 10console.log(triple(5)); // 153. Module Pattern (Pre-ES6)
const UserModule = (function() { const users = [];
function addUser(name) { users.push({ name, id: users.length + 1 }); }
function listUsers() { return [...users]; }
return { addUser, listUsers };})();
UserModule.addUser("Alice");console.log(UserModule.listUsers()); // [{ name: "Alice", id: 1 }]The Lexical Environment
Every time a function runs, JavaScript creates a new lexical environment containing:
- An environment record — the local variables, parameters, and inner function declarations
- A reference to the outer environment (parent scope)
const global = "global";
function outer() { const outerVar = "outer";
function inner() { const innerVar = "inner"; console.log(innerVar); // from inner's environment record console.log(outerVar); // from outer's environment record (via reference) console.log(global); // from global environment record }
inner();}
outer();The scope chain is this linked list of environments. When resolving a variable, JavaScript walks up this chain until it finds the variable or reaches the global scope.
IIFE (Immediately Invoked Function Expression)
An IIFE is a function that runs as soon as it’s defined. It creates a private scope:
(function() { const privateData = "secret"; console.log("IIFE ran:", privateData);})();// privateData is not accessible hereModern JavaScript rarely needs IIFEs because let/const provide block scoping. However, you still see them in legacy code and some build tools.
Common Pitfalls
Loop + Closure (Classic Problem)
// Problem: all buttons log 5for (var i = 0; i < 5; i++) { document.querySelectorAll("button")[i].onclick = function() { console.log(i); // always 5 };}
// Fix 1: Use let (block scope)for (let i = 0; i < 5; i++) { /* ... */ }
// Fix 2: IIFE closurefor (var i = 0; i < 5; i++) { (function(index) { document.querySelectorAll("button")[index].onclick = function() { console.log(index); }; })(i);}Accidental Global Variable
function bad() { leaked = "I'm global!"; // no var/let/const — becomes global!}bad();console.log(leaked); // "I'm global!" — probably unintendedAlways use strict mode ("use strict") to catch this:
"use strict";function good() { leaked = "error!"; // ❌ ReferenceError}Quick Reference
| Concept | Keyword | Scope | Hoisting | TDZ |
|---|---|---|---|---|
| Global | none | Everywhere | N/A | N/A |
| Function | var | Function | undefined | No |
| Block | let | Block | Hoisted | Yes |
| Block | const | Block | Hoisted | Yes |
| Closure | function | Lexical | Definition | No |
Practice Exercises
Predict the output: What will
console.log(x)show at each labelled point?var x = 1;function test() {console.log(x); // ?var x = 2;console.log(x); // ?}test();console.log(x); // ?Fix the counter: The following counter should log 0, 1, 2 but doesn’t. Why?
for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 100);}Build a memoize function: Create a function
memoize(fn)that returns a cached version of any pure function using closures.