Scope determines where variables are accessible. Closures allow functions to remember variables from their outer scope even after that outer function has returned. Together they underpin JavaScript’s most powerful patterns.

Types of Scope

Global Scope

Variables declared outside any function or block:

  let globalVar = 'I am global';

function showGlobal() {
    console.log(globalVar);
}

showGlobal(); // 'I am global'
  

Avoid polluting global scope — use modules or IIFEs in legacy code.

Function Scope

var is function-scoped — visible throughout the entire function:

  function example() {
    var fnScoped = 'inside function';
    if (true) {
        var alsoFnScoped = 'still function scope';
    }
    console.log(alsoFnScoped); // accessible
}
// console.log(fnScoped); // ReferenceError
  

Block Scope

let and const are block-scoped — limited to nearest {}:

  if (true) {
    let blockVar = 'block';
    const alsoBlock = 'block';
}
// console.log(blockVar); // ReferenceError

for (let i = 0; i < 3; i++) {
    // each iteration gets its own `i`
}
  

Best practice: Use const by default; let when reassignment needed; avoid var.

Temporal Dead Zone (TDZ)

let/const exist but are inaccessible before their declaration line:

  // console.log(x); // ReferenceError — TDZ
let x = 10;
  

var is hoisted and initialized to undefined — another reason to avoid it.

Lexical Scope

JavaScript uses lexical (static) scope — a function’s scope is determined by where it is written, not where it is called:

  let outer = 'outer';

function outerFn() {
    let inner = 'inner';
    function innerFn() {
        console.log(outer, inner); // both accessible
    }
    return innerFn;
}

const fn = outerFn();
fn(); // 'outer', 'inner' — closure preserves inner
  

Closures

A closure is a function plus its surrounding lexical environment:

  function createCounter() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

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

const counter2 = createCounter();
console.log(counter2()); // 1 — independent closure
  

Each call to createCounter() creates a new closure with its own count.

Practical Closure Uses

Module Pattern (Private State)

  function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit(amount) {
            balance += amount;
            return balance;
        },
        withdraw(amount) {
            if (amount <= balance) balance -= amount;
            return balance;
        },
        getBalance() {
            return balance;
        }
    };
}

const account = createBankAccount(100);
account.deposit(50);   // 150
account.withdraw(30);  // 120
// account.balance — undefined (private)
  

Factory Functions

  function multiply(factor) {
    return (number) => number * factor;
}

const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
  

Event Handlers with State

  function setupButton(label) {
    let clicks = 0;
    return function handleClick() {
        clicks++;
        console.log(`${label}: ${clicks} clicks`);
    };
}

document.querySelector('#btn').addEventListener('click', setupButton('Submit'));
  

Closures in Loops (Common Pitfall)

  // Problem: var is function-scoped — all callbacks share same i
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// Fix 1: use let (block scope per iteration)
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// Fix 2: IIFE captures current i
for (var i = 0; i < 3; i++) {
    ((j) => setTimeout(() => console.log(j), 100))(i);
}
  

Scope Chain

When a variable is referenced, JavaScript searches the scope chain:

  1. Current function/block scope
  2. Outer function scope
  3. … up to global
  4. ReferenceError if not found
  let a = 1;

function outer() {
    let b = 2;
    function inner() {
        let c = 3;
        console.log(a, b, c); // 1, 2, 3
    }
    inner();
}
  

Inner scope can read outer variables; outer scope cannot read inner variables.

Closures and Memory

Closures keep outer variables alive as long as the inner function is reachable. This can cause memory leaks if closures hold large objects unnecessarily:

  function leak() {
    const hugeData = new Array(1e6).fill('x');
    return function() {
        console.log(hugeData.length); // keeps hugeData in memory
    };
}
  

Release references when done: handler = null.

IIFE (Immediately Invoked Function Expression)

Legacy pattern for private scope before modules:

  (function() {
    const private = 'hidden';
    window.myApp = { getPrivate: () => private };
})();
  

Modern code uses ES modules instead.

Best Practices

  1. Prefer const/let over var
  2. Keep closures small — return only what’s needed
  3. Watch loop + async patterns — use let or for...of
  4. Use modules for encapsulation in modern projects

Troubleshooting

Unexpected undefined in closure

  • Variable changed before async callback runs — capture value in loop with let or parameter

Memory usage growing

  • Closures holding DOM references to removed elements — null out references

this behaves unexpectedly in closure

  • Arrow functions inherit lexical this; regular functions have own this

Understanding scope and closures is essential for callbacks, async code, modules, and advanced JavaScript patterns.