Home Module 09 Scope

1. Introduction

Scope is the set of rules that determines where a variable can be accessed. When you declare a variable, it is only accessible from certain parts of the code — its "scope." Understanding scope prevents bugs like accidentally overwriting a variable from another part of the program, or trying to access a variable that doesn't exist in the current context.

JavaScript has four types of scope:

  • Global scope — accessible anywhere in the script
  • Function scope — accessible only inside a function
  • Block scope — accessible only inside a { } block (with let/const)
  • Lexical scope / closures — inner functions can access outer function variables

2. Theory

2.1 Global scope

Variables declared outside any function or block are in the global scope. They are accessible from anywhere in the script.

const appName = 'MyApp'; // global

function showName() {
  console.log(appName); // accessible here
}

showName();             // 'MyApp'
console.log(appName);  // 'MyApp'

Global variables are convenient but dangerous in large programs — any code can accidentally modify them. Minimise globals.

2.2 Function scope

Variables declared inside a function exist only for the duration of that function call. They are created when the function runs and destroyed when it returns.

function greet() {
  const message = 'Hello!'; // function-scoped
  console.log(message);     // works inside
}

greet();
console.log(message); // ReferenceError — message doesn't exist here

Each function call gets its own copy of its local variables:

function counter() {
  let count = 0; // new 'count' each time
  count++;
  console.log(count);
}

counter(); // 1
counter(); // 1 (fresh copy each time — not accumulating)
counter(); // 1

2.3 Block scope (let and const)

let and const are block-scoped — they exist only inside the { } block where they are declared:

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

// var is NOT block-scoped (function-scoped instead)
if (true) {
  var z = 30;
}
console.log(z); // 30 — var leaks out of blocks!

This is a major reason to avoid var — it ignores block boundaries.

2.4 Scope chain (lexical lookup)

When JavaScript looks up a variable name, it starts in the current scope and works outward until it finds it or reaches global scope:

const global = 'I am global';

function outer() {
  const outerVar = 'I am in outer';

  function inner() {
    const innerVar = 'I am in inner';
    console.log(innerVar);  // found in own scope
    console.log(outerVar);  // found in outer scope
    console.log(global);    // found in global scope
  }

  inner();
  console.log(outerVar);  // works
  // console.log(innerVar); // ReferenceError — inner is out of reach
}

outer();

2.5 Closures

A closure is when an inner function "remembers" the variables from its outer function, even after the outer function has finished executing.

function makeCounter() {
  let count = 0; // this variable is "closed over"

  return function() {
    count++;
    console.log(count);
  };
}

const counter = makeCounter(); // outer function runs, returns inner function
counter(); // 1
counter(); // 2
counter(); // 3
// count persists because the inner function keeps a reference to it

// Each call to makeCounter() creates a separate closure
const counter2 = makeCounter();
counter2(); // 1 — independent count

Closures are used everywhere in JavaScript: event handlers, callbacks, module patterns, and React hooks all rely on closures.

2.6 Variable shadowing

An inner variable with the same name as an outer one "shadows" the outer one inside its scope:

let name = 'Alice'; // outer

function greet() {
  let name = 'Bob'; // shadows the outer 'name' inside this function
  console.log(name); // 'Bob'
}

greet();
console.log(name); // 'Alice' — outer unchanged

Shadowing is legal but can be confusing. Use different names when possible.

2.7 The global object

In a browser, global variables become properties of the window object. Using var at the top level adds it to window; let and const do not.

var x = 10;
console.log(window.x); // 10 — var becomes a window property

let y = 20;
console.log(window.y); // undefined — let does not

This is another reason to avoid var — polluting the global object can cause conflicts with third-party scripts.

2.8 Practical closure — private counter

function createScoreBoard(playerName) {
  let score = 0; // private — not accessible from outside

  return {
    addPoints(n) { score += n; },
    deduct(n)    { score = Math.max(0, score - n); },
    getScore()   { return score; },
    display()    { console.log(`${playerName}: ${score} pts`); },
  };
}

const player1 = createScoreBoard('Alice');
player1.addPoints(10);
player1.addPoints(5);
player1.deduct(3);
player1.display(); // 'Alice: 12 pts'
console.log(player1.score); // undefined — score is private

3. Real World Example

// Module pattern using closures — common before ES Modules
const shoppingCart = (function () {
  const items = []; // private — cannot be accessed from outside

  return {
    add(item) {
      items.push(item);
    },
    remove(name) {
      const idx = items.findIndex(i => i.name === name);
      if (idx !== -1) items.splice(idx, 1);
    },
    total() {
      return items.reduce((sum, i) => sum + i.price, 0);
    },
    count() {
      return items.length;
    },
  };
})(); // IIFE — Immediately Invoked Function Expression

shoppingCart.add({ name: 'Book', price: 12.99 });
shoppingCart.add({ name: 'Pen',  price: 1.99  });
console.log(shoppingCart.count()); // 2
console.log(shoppingCart.total()); // 14.98
console.log(shoppingCart.items);   // undefined — private!

4. Code Example

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Scope & Closures Demo</title></head>
<body>
  <h1>Closure Counter</h1>
  <div id="counters"></div>
  <button id="add-counter">+ Add Counter</button>

  <script>
    // Factory function using closures
    function createCounter(label) {
      let count = 0; // private to each counter instance

      // Return a DOM element that manages its own count
      const wrapper = document.createElement('div');
      wrapper.style.cssText = 'margin: 1rem 0; display: flex; gap: 1rem; align-items: center;';

      const display = document.createElement('span');
      display.textContent = `${label}: 0`;

      const incBtn = document.createElement('button');
      incBtn.textContent = '+1';

      const decBtn = document.createElement('button');
      decBtn.textContent = '-1';

      const resetBtn = document.createElement('button');
      resetBtn.textContent = 'Reset';

      // Each closure captures its own 'count' variable
      incBtn.addEventListener('click', () => {
        count++;
        display.textContent = `${label}: ${count}`;
      });
      decBtn.addEventListener('click', () => {
        count--;
        display.textContent = `${label}: ${count}`;
      });
      resetBtn.addEventListener('click', () => {
        count = 0;
        display.textContent = `${label}: ${count}`;
      });

      wrapper.append(display, incBtn, decBtn, resetBtn);
      return wrapper;
    }

    let counterIndex = 1;

    // Add initial counter
    document.querySelector('#counters').appendChild(createCounter('Counter 1'));

    document.querySelector('#add-counter').addEventListener('click', () => {
      counterIndex++;
      document.querySelector('#counters').appendChild(
        createCounter('Counter ' + counterIndex)
      );
    });
  </script>
</body>
</html>

5. Code Breakdown

let count = 0 inside createCounter()

This variable is in the function scope of createCounter. Each call to createCounter() creates a completely new, independent count variable. Adding Counter 2 has no effect on Counter 1's count — they are separate closures.

Event handlers closing over count and label

The three event handler arrow functions (incBtn.addEventListener, etc.) all reference count and label from the enclosing createCounter call. These variables are "closed over" — even after createCounter returns, the event handlers keep the variables alive and accessible.

createCounter returns a DOM element

The function creates and wires up an entire mini-component (DOM element + behaviour) and returns it. The caller just appends it to the page. This is the factory function pattern — a common way to create multiple independent instances of stateful UI components before frameworks like React.

counterIndex — global state

counterIndex is declared in the outer script scope. It is shared state — it persists between button clicks. This is one of the few valid uses of a higher-scope variable: tracking application-level state that multiple event handlers need to share.

6. Common Mistakes

Mistake 1 — Classic closure loop bug (with var)

// Bug — all handlers share the same 'i' (var is function-scoped)
for (var i = 0; i < 3; i++) {
  buttons[i].addEventListener('click', () => console.log(i));
}
// Clicking any button logs 3 (the final value of i after the loop)

// Fix 1 — use let (block-scoped, each iteration gets its own i)
for (let i = 0; i < 3; i++) {
  buttons[i].addEventListener('click', () => console.log(i));
}
// Now each closure captures a different i: 0, 1, 2

Mistake 2 — Accidental global variable

function save() {
  data = 'important'; // no let/const — becomes global!
}
// Later, another function can overwrite it accidentally

// Fix
function save() {
  const data = 'important'; // local
}

Mistake 3 — Shadowing a variable unintentionally

const name = 'Alice';

function process() {
  const name = 'Bob'; // shadows — OK if intentional
  console.log(name);  // 'Bob'
}
// If you meant to use the outer 'name', rename the local one

Mistake 4 — Trying to access function-local variables from outside

function compute() {
  const result = 42;
}
compute();
console.log(result); // ReferenceError — result is local

// Fix — return the value
function compute() {
  return 42;
}
const result = compute();
console.log(result); // 42

7. Best Practices

  1. Minimise global variables — use functions and modules to keep state encapsulated.
  2. Use let and const — block scoping prevents leaking variables out of loops and if blocks.
  3. Declare variables at the top of their scope — makes it clear what variables a block uses.
  4. Use closures deliberately for private state — expose only what needs to be public.
  5. Avoid variable shadowing unless intentional — use unique names to prevent confusion.
  6. Use strict mode ('use strict';) to catch undeclared variable assignments that would otherwise become accidental globals.
  7. Keep functions small — smaller functions have smaller scopes, which are easier to reason about.

8. Practice Exercise

  1. Identify the scope of each variable in this code:
    const a = 1;
    function outer() {
      const b = 2;
      function inner() {
        const c = 3;
        console.log(a, b, c);
      }
      inner();
    }
    outer();
  2. Write a makeAdder(n) closure factory: const add5 = makeAdder(5); add5(3) should return 8. Make add10 and test both.
  3. Write a createTimer() function that returns an object with start(), stop(), and getElapsed() methods. Use Date.now() and private variables to track time.
  4. Demonstrate the var vs let loop closure bug: create an array of three functions using both var and let in a for loop, then call each. Observe the different outputs.

Bonus

  • Implement the module pattern: create a bankAccount module with private balance, and public deposit(n), withdraw(n), and getBalance() methods. Ensure balance cannot be set from outside.

9. Assignment

Build a "Stopwatch" using closures.

  1. Write a createStopwatch() factory function that returns an object with:
    • start() — begins timing
    • stop() — pauses timing
    • reset() — sets elapsed time back to 0
    • getTime() — returns elapsed milliseconds
  2. The internal variables (start time, accumulated time, running state) must be private — inaccessible from outside.
  3. Create an HTML page with Start, Stop, and Reset buttons, plus a display that shows elapsed seconds.
  4. Update the display every 100 ms using setInterval while running.
  5. Allow multiple independent stopwatch instances on the same page using the factory function.

Deliverable: One HTML file. Each stopwatch is an independent closure instance.

10. Interview Questions

  1. What is scope in JavaScript?
    Scope determines where a variable can be accessed. JavaScript has global scope, function scope, and block scope (with let/const). A variable is only accessible within the scope where it was declared and in any nested inner scopes.
  2. What is a closure?
    A closure is when an inner function retains access to its outer function's variables even after the outer function has returned. The inner function "closes over" the outer variables, keeping them alive as long as the inner function is referenced.
  3. What is the difference between function scope and block scope?
    Function scope means a variable exists for the entire function (this is how var works). Block scope means a variable exists only within the { } block where it was declared (this is how let and const work).
  4. Why does this loop bug happen with var, and how do you fix it?
    for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } — var is function-scoped, so all three callbacks share the same i. After the loop, i is 3, so all three log 3. Fix: use let (each iteration gets its own block-scoped i).
  5. What are some practical uses of closures?
    Private state (module pattern), factory functions (createCounter, makeAdder), event handlers capturing loop variables, memoization (caching function results), and React hooks (useState, useEffect all use closures internally).
  6. What is variable shadowing?
    When an inner variable has the same name as an outer one, the inner one "shadows" the outer within its scope. The outer variable is unchanged and accessible again once outside the inner scope.

11. Additional Resources

  • MDN — Closures — comprehensive guide with examples
  • javascript.info — Variable scope, closure — excellent visual explanation
  • MDN — Scope
  • javascript.info — The old "var" — explains var's function scoping and hoisting
  • You Don't Know JS — Scope & Closures (free online) — deep dive