Skip to main content
โš™๏ธbeginner

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:

  1. Global scope โ€” accessible everywhere
  2. Function scope โ€” accessible within the function
  3. Block scope โ€” accessible within the {} block (let and const)

Global Scope

Variables declared outside any function or block are global:

hljs javascript
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:

hljs javascript
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:

hljs javascript
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):

hljs javascript
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:

hljs javascript
console.log(add(2, 3)); // 5 โ€” works!

function add(a, b) {
  return a + b;
}

Internally, JavaScript processes this as:

hljs javascript
// 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:

hljs javascript
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:

hljs javascript
// 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).

hljs javascript
{
  // TDZ starts here for myVar
  console.log(myVar); // ReferenceError!
  let myVar = "hello"; // TDZ ends here
}

โœ…Hoisting summary

Hoisted?Initial valueAccessible before declaration?
varYesundefinedYes (value is undefined)
letYesUninitialized (TDZ)No (ReferenceError)
constYesUninitialized (TDZ)No (ReferenceError)
Function declarationYes (fully)The functionYes
Function expressionNoN/ANo

Lexical Scope in Practice

Closures (covered in the next lesson) rely on lexical scope. Here's a preview:

hljs javascript
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.

โ–ถTry it yourself

Key Takeaways

  • Global scope โ€” accessible everywhere; use sparingly
  • Function scope โ€” variables live inside their function
  • Block scope โ€” let/const are limited to their {} block
  • Scope chain โ€” JavaScript walks up scopes to resolve variables (lexical scoping)
  • Hoisting โ€” var declarations and function declarations move to the top of their scope
  • let/const have the Temporal Dead Zone โ€” don't access them before declaration

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’