Scope and Hoisting in JavaScript
- var is function-scoped; let and const are block-scoped.
- Function declarations are fully hoisted — you can call them before they are defined.
- var is hoisted and initialised to undefined; let/const are hoisted but in the temporal dead zone.
Scope determines where a variable is accessible. JavaScript has three scope types: global, function (var), and block (let/const). Hoisting moves declarations to the top of their scope before execution — function declarations are fully hoisted, var declarations are hoisted but initialised as undefined, let and const are hoisted but not initialised (temporal dead zone).
var vs let vs const — The Scope Difference
// var: function-scoped — ignores block boundaries function testVar() { if (true) { var x = 10; // declared inside if block } console.log(x); // 10 — accessible outside the block! } testVar(); // let: block-scoped — respects {} boundaries function testLet() { if (true) { let y = 10; } // console.log(y); // ReferenceError — y not accessible here } // const: block-scoped + cannot be reassigned const MAX = 100; // MAX = 200; // TypeError: Assignment to constant variable // const with objects — the binding is const, not the object const user = { name: 'Alice' }; user.name = 'Bob'; // fine — mutating the object, not the binding console.log(user.name); // Bob
Bob
Hoisting — What Actually Happens
Before executing any code, JavaScript's engine scans for declarations and processes them. Function declarations are fully hoisted. var declarations are hoisted but set to undefined. let and const are hoisted but not accessible until the declaration line.
// Function declaration — fully hoisted, works before definition console.log(greet('Forge')); // 'Hello, Forge!' — works! function greet(name) { return `Hello, ${name}!`; } // var — hoisted but undefined console.log(city); // undefined — hoisted, not initialised var city = 'London'; console.log(city); // 'London' // let/const — temporal dead zone (TDZ) // console.log(country); // ReferenceError: Cannot access before initialization let country = 'UK'; console.log(country); // 'UK' // Function expression — NOT hoisted (it is a variable assignment) // console.log(sayHi()); // TypeError: sayHi is not a function const sayHi = function() { return 'Hi!'; }; console.log(sayHi()); // 'Hi!'
undefined
London
UK
Hi!
Lexical Scope and Closures
const globalVar = 'global'; function outer() { const outerVar = 'outer'; function inner() { const innerVar = 'inner'; // Can access all three — lexical scope chain console.log(innerVar); // 'inner' console.log(outerVar); // 'outer' console.log(globalVar); // 'global' } inner(); // console.log(innerVar); // ReferenceError — inner scope not accessible here } outer(); // Scope chain: inner → outer → global → undefined // JS looks up the chain until it finds the variable or exhausts the chain
outer
global
🎯 Key Takeaways
- var is function-scoped; let and const are block-scoped.
- Function declarations are fully hoisted — you can call them before they are defined.
- var is hoisted and initialised to undefined; let/const are hoisted but in the temporal dead zone.
- The temporal dead zone is the period between hoisting and the actual declaration line.
- Prefer const by default; use let when you need to reassign; avoid var in modern code.
Interview Questions on This Topic
- QWhat is the difference between var, let, and const in terms of scope?
- QWhat is the temporal dead zone?
- QWhat is the difference between hoisting of function declarations and function expressions?
Frequently Asked Questions
What is the temporal dead zone?
The TDZ is the period from the start of the block scope until the let or const declaration is reached. The variable exists (it has been hoisted) but cannot be accessed — reading it throws ReferenceError. This is by design to prevent bugs that var's hoisting allows.
Why does var inside a for loop leak out of the loop?
Because var is function-scoped, not block-scoped. The for loop's curly braces do not create a new scope for var. The variable lives in the containing function. This is the same reason the loop closure trap happens. Use let in for loops to get a new binding per iteration.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.