Scope & Hoisting
Understand where variables live, why hoisting surprises developers, and how JavaScript resolves identifiers up the scope chain.
What is Scope?
Scope determines where a variable is accessible in your code. Variables declared in one scope are not visible in others unless they flow through the scope chain.
There are three types of scope in modern JavaScript:
- Global scope โ accessible everywhere
- Function scope โ accessible within the function
- Block scope โ accessible within the
{}block (letandconst)
Global Scope
Variables declared outside any function or block are global:
const globalName = "Alice"; // global
function greet() {
console.log(globalName); // accessible inside function
}
greet(); // "Alice"
console.log(globalName); // "Alice"
โ ๏ธMinimize global variables
Global variables are accessible everywhere, which makes code hard to reason about and debug. Accidentally modifying a global from multiple places is a common bug. Keep your scope as small as possible.
Function Scope
Variables declared inside a function are only accessible within that function:
function calculateTax(income) {
const taxRate = 0.2; // function-scoped
const tax = income * taxRate;
return tax;
}
console.log(calculateTax(50000)); // 10000
// console.log(taxRate); // ReferenceError: taxRate is not defined
Block Scope (let and const)
let and const are block-scoped โ limited to the {} they're declared in:
if (true) {
let blockVar = "I'm block-scoped";
const blockConst = "Me too";
console.log(blockVar); // โ accessible here
}
// console.log(blockVar); // โ ReferenceError
// Classic block scope example with loops
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 โ each iteration has its own `i`
// Compare with var (no block scope):
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Prints: 3, 3, 3 โ all share the same `j`
The Scope Chain
When a variable is referenced, JavaScript looks for it in the current scope, then walks up through parent scopes until it finds it (or throws a ReferenceError):
const globalVar = "I'm global";
function outer() {
const outerVar = "I'm outer";
function inner() {
const innerVar = "I'm inner";
// Can access all three:
console.log(innerVar); // โ
console.log(outerVar); // โ (outer scope)
console.log(globalVar); // โ (global scope)
}
inner();
// console.log(innerVar); // โ Not accessible here
}
outer();
This is called lexical scoping โ the scope is determined by where the code is written, not where it's called from.
Hoisting
Hoisting is JavaScript's default behavior of moving declarations to the top of their scope during the compilation phase (before execution). Only declarations are hoisted, not initializations.
Function Declaration Hoisting
Function declarations are fully hoisted โ you can call them before they appear in code:
console.log(add(2, 3)); // 5 โ works!
function add(a, b) {
return a + b;
}
Internally, JavaScript processes this as:
// What JS actually does:
function add(a, b) { return a + b; } // hoisted to top
console.log(add(2, 3)); // 5
var Hoisting
var declarations are hoisted, but initialized to undefined:
console.log(x); // undefined (not an error!)
var x = 5;
console.log(x); // 5
// What JS does:
// var x; // declaration hoisted, value is undefined
// console.log(x); // undefined
// x = 5; // assignment stays in place
// console.log(x); // 5
let and const โ Temporal Dead Zone
let and const are hoisted but NOT initialized. Accessing them before declaration throws a ReferenceError:
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
console.log(y); // 10
The period between the start of the block and the let/const declaration is called the Temporal Dead Zone (TDZ).
{
// TDZ starts here for myVar
console.log(myVar); // ReferenceError!
let myVar = "hello"; // TDZ ends here
}
โ Hoisting summary
| Hoisted? | Initial value | Accessible before declaration? | |
|---|---|---|---|
var | Yes | undefined | Yes (value is undefined) |
let | Yes | Uninitialized (TDZ) | No (ReferenceError) |
const | Yes | Uninitialized (TDZ) | No (ReferenceError) |
| Function declaration | Yes (fully) | The function | Yes |
| Function expression | No | N/A | No |
Lexical Scope in Practice
Closures (covered in the next lesson) rely on lexical scope. Here's a preview:
function makeAdder(x) {
// x is in scope here
return function(y) {
return x + y; // x is still accessible!
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3)); // 8
console.log(add10(3)); // 13
add5 and add10 each retain access to their own x even after makeAdder has returned. This is a closure โ the inner function "closes over" the outer scope.
Key Takeaways
- Global scope โ accessible everywhere; use sparingly
- Function scope โ variables live inside their function
- Block scope โ
let/constare limited to their{}block - Scope chain โ JavaScript walks up scopes to resolve variables (lexical scoping)
- Hoisting โ
vardeclarations and function declarations move to the top of their scope let/consthave the Temporal Dead Zone โ don't access them before declaration
Ready to test your knowledge?
Take a quiz on what you just learned.