Skip to main content

Scope and Hoisting in JavaScript

Created: December 18, 2025 5 min read

Scope determines where variables are accessible. Hoisting is how JavaScript moves declarations to the top. Understanding both is crucial.

What is Scope?

Scope is the region where a variable is accessible. JavaScript has three types of scope:

  1. Global Scope - accessible everywhere
  2. Function Scope - accessible within a function
  3. Block Scope - accessible within a block (if, for, while, etc.)

Global Scope

Variables declared outside functions are global:

const globalVar = "I'm global";

function test() {
    console.log(globalVar); // "I'm global"
}

test();
console.log(globalVar); // "I'm global"

Global Object

In browsers, global variables become properties of window:

var x = 5;
console.log(window.x); // 5

let y = 10;
console.log(window.y); // undefined (let doesn't attach to window)

In Node.js, use global:

var x = 5;
console.log(global.x); // 5

Function Scope

Variables declared inside a function are local to that function:

function myFunction() {
    const localVar = "I'm local";
    console.log(localVar); // "I'm local"
}

myFunction();
console.log(localVar); // ReferenceError: localVar is not defined

Function Scope with var

var is function-scoped:

function example() {
    if (true) {
        var x = 10;
    }
    console.log(x); // 10 (accessible outside if block)
}

example();

Block Scope

let and const are block-scoped:

function example() {
    if (true) {
        let x = 10;
        const y = 20;
    }
    console.log(x); // ReferenceError
    console.log(y); // ReferenceError
}

example();

Block Scope Examples

// if block
if (true) {
    let x = 1;
}
console.log(x); // ReferenceError

// for loop
for (let i = 0; i < 3; i++) {
    // i is scoped to the loop
}
console.log(i); // ReferenceError

// while loop
while (true) {
    let x = 1;
    break;
}
console.log(x); // ReferenceError

// try-catch
try {
    throw new Error("test");
} catch (e) {
    let error = e;
}
console.log(error); // ReferenceError

Scope Chain

Inner scopes can access outer scopes:

const global = "global";

function outer() {
    const outerVar = "outer";
    
    function inner() {
        const innerVar = "inner";
        
        console.log(innerVar); // "inner"
        console.log(outerVar); // "outer"
        console.log(global); // "global"
    }
    
    inner();
}

outer();

Scope Chain Lookup

JavaScript looks for variables from inner to outer scope:

const x = "global";

function test() {
    const x = "function";
    
    if (true) {
        const x = "block";
        console.log(x); // "block" (closest scope)
    }
    
    console.log(x); // "function"
}

test();
console.log(x); // "global"

Hoisting

Hoisting moves declarations to the top of their scope before execution.

var Hoisting

var declarations are hoisted and initialized with undefined:

console.log(x); // undefined (not an error!)
var x = 5;
console.log(x); // 5

This is equivalent to:

var x;
console.log(x); // undefined
x = 5;
console.log(x); // 5

Function Hoisting

Function declarations are fully hoisted:

console.log(greet("Alice")); // "Hello, Alice!"

function greet(name) {
    return `Hello, ${name}!`;
}

This works because the entire function is hoisted.

let and const Hoisting

let and const are hoisted but not initialized (Temporal Dead Zone):

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 10;

Function Expression Hoisting

Function expressions are NOT hoisted:

console.log(greet("Alice")); // TypeError: greet is not a function

const greet = function(name) {
    return `Hello, ${name}!`;
};

Temporal Dead Zone (TDZ)

The period between entering a scope and reaching the declaration:

function example() {
    console.log(x); // ReferenceError (in TDZ)
    
    let x = 5; // TDZ ends here
    
    console.log(x); // 5
}

example();

Practical Examples

Avoiding Global Pollution

// Bad - pollutes global scope
var globalCounter = 0;

function increment() {
    globalCounter++;
}

// Good - encapsulated in function
function createCounter() {
    let count = 0;
    
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Module Pattern

const calculator = (function() {
    let result = 0; // Private variable
    
    return {
        add(x) {
            result += x;
            return this;
        },
        subtract(x) {
            result -= x;
            return this;
        },
        getResult() {
            return result;
        }
    };
})();

console.log(calculator.add(5).subtract(2).getResult()); // 3
console.log(calculator.result); // undefined (private)

Loop Variable Scope

// Bad - var leaks out
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3

// Good - let is block-scoped
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

Closure with Block Scope

function createFunctions() {
    const functions = [];
    
    for (let i = 0; i < 3; i++) {
        functions.push(() => i);
    }
    
    return functions;
}

const fns = createFunctions();
console.log(fns[0]()); // 0
console.log(fns[1]()); // 1
console.log(fns[2]()); // 2

Best Practices

Use const by Default

// Good
const x = 5;
let y = 10;

// Avoid
var z = 15;

Minimize Global Variables

// Bad
var globalConfig = { ... };

// Good
const config = (function() {
    return { ... };
})();

Understand Hoisting

// Avoid relying on hoisting
function test() {
    console.log(x); // Don't do this
    var x = 5;
}

// Better - declare at top
function test() {
    var x;
    console.log(x); // undefined
    x = 5;
}

Use Block Scope

// Good - block scope
if (condition) {
    let x = 5;
}

// Avoid - function scope
if (condition) {
    var x = 5;
}

Summary

  • Global Scope: accessible everywhere
  • Function Scope: accessible within function (var)
  • Block Scope: accessible within block (let, const)
  • Scope Chain: inner scopes access outer scopes
  • Hoisting: declarations moved to top
  • TDZ: period before let/const initialization
  • Best practice: use const/let, avoid var

Official Documentation

Next Steps

  1. Closures: Understanding Function Scope
  2. The ’this’ Keyword and Context Binding
  3. Functions: Definition, Parameters, Return Values

Resources

Comments

Share this article

Scan to read on mobile