Skip to main content
โšก Calmops

Scope and Hoisting in JavaScript

Scope and Hoisting in JavaScript

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

Comments