Understanding Scope
Scope determines where variables are accessible. JavaScript has three types: global, function, and block scope. Understanding scope prevents one of the most common categories of bugs.
// Global scope — accessible everywhere
const globalVar = "I'm global";
function myFunction() {
// Function scope — only inside this function
const functionVar = "I'm function-scoped";
if (true) {
// Block scope — only inside this if block
const blockVar = "I'm block-scoped";
let alsoBlock = "Also block-scoped";
var leaksOut = "I leak to function scope!"; // var = function scope
console.log(globalVar); // ✅ accessible
console.log(functionVar); // ✅ accessible
console.log(blockVar); // ✅ accessible
}
console.log(globalVar); // ✅ accessible
console.log(functionVar); // ✅ accessible
// console.log(blockVar); // ❌ ReferenceError
console.log(leaksOut); // ✅ var leaks to function scope!
}
// console.log(functionVar); // ❌ ReferenceError
// console.log(leaksOut); // ❌ ReferenceError (function-scoped)
Scope Chain & Lexical Scope
// Inner scopes can access outer scope (scope chain):
const outerVar = "outer";
function outer() {
const middleVar = "middle";
function inner() {
const innerVar = "inner";
// Can access ALL outer variables:
console.log(outerVar); // ✅ "outer"
console.log(middleVar); // ✅ "middle"
console.log(innerVar); // ✅ "inner"
}
inner();
// console.log(innerVar); // ❌ can't look inward
}
// Variable shadowing:
const x = "global";
function shadow() {
const x = "local"; // shadows the global x
console.log(x); // "local"
}
shadow();
console.log(x); // "global" — global unchanged
Hoisting
// Function declarations are hoisted COMPLETELY:
sayHello(); // ✅ Works — "Hello!"
function sayHello() { console.log("Hello!"); }
// var declarations are hoisted (but NOT their values):
console.log(myVar); // undefined (not an error!)
var myVar = "value";
console.log(myVar); // "value"
// let and const are NOT accessible before declaration:
// console.log(myLet); // ❌ ReferenceError: Cannot access before init
let myLet = "value";
// Temporal Dead Zone (TDZ):
// let and const variables exist in TDZ from start of scope
// until declaration is reached. Accessing in TDZ = error.
// Function expression — NOT hoisted:
// greet(); // ❌ TypeError: greet is not a function
const greet = function() { console.log("Hi"); };
greet(); // ✅ Works after declaration
The Call Stack
// The call stack tracks function calls
function third() {
console.trace(); // shows the call stack
return "third";
}
function second() {
return third();
}
function first() {
return second();
}
first();
// Call stack (bottom to top):
// first → second → third
// Stack overflow — too many nested calls:
function infinite() {
return infinite(); // calls itself forever
}
// infinite(); // ❌ RangeError: Maximum call stack size exceeded
// This is why recursion needs a base case:
function countdown(n) {
if (n <= 0) return; // base case — stops recursion
console.log(n);
countdown(n - 1);
}
countdown(5); // 5, 4, 3, 2, 1
⚡ Key Takeaways
- Scope determines variable visibility — inner scopes can see outer, not vice versa
- Use
const/letfor block scope; avoidvar's function scope - Function declarations are fully hoisted — callable before declaration
varhoists to undefined;let/constare in TDZ until declared- The call stack tracks active function calls — overflow means too deep recursion
🎯 Practice Exercises
EXERCISE 1
Predict the output before running: declare a variable with var inside an if block, then log it outside. Do the same with let. Explain the difference.
EXERCISE 2
Create a nested function structure 3 levels deep. Show that the innermost function can read variables from all outer scopes but outer scopes can't read inner variables.