We use cookies to enhance your experience on the site
CodeWorlds

Closures and Lexical Scope

In Jurassic Park, every team member has their own personal log - secure, private, accessible only through specific procedures. Closures in JavaScript work exactly this way: they give functions access to variables from their outer scope, even after that outer function has finished executing.

Closures - The Core Concept

A closure forms when a function "remembers" variables from the scope where it was created:

1function createDinoLog(dinoName) {
2  // This variable lives in the closure - accessible from inner function
3  const log = [];
4
5  return {
6    addEntry(event) {
7      log.push({ event, time: new Date().toISOString() });
8      console.log(`[${dinoName}] ${event} logged`);
9    },
10    getLog() {
11      return [...log]; // return copy
12    },
13    summary() {
14      return `${dinoName}: ${log.length} log entries`;
15    }
16  };
17}
18
19const rexLog = createDinoLog('Rex');
20const blueLog = createDinoLog('Blue');
21
22rexLog.addEntry('Fed - 200kg beef');
23rexLog.addEntry('Health check - 95%');
24blueLog.addEntry('Fed - 5kg chicken');
25
26console.log(rexLog.summary()); // "Rex: 2 log entries"
27console.log(blueLog.summary()); // "Blue: 1 log entries"
28
29// Each log is independent and private!
30// console.log(log); // ReferenceError - can't access directly

Private Data with Closures

Closures are the classic way to implement private data in JavaScript:

1function createCounter(initial = 0, step = 1) {
2  let count = initial; // private
3  let history = [];    // private
4
5  return {
6    increment() {
7      count += step;
8      history.push(count);
9      return count;
10    },
11    decrement() {
12      count -= step;
13      history.push(count);
14      return count;
15    },
16    reset() {
17      count = initial;
18      history.push(count);
19      return count;
20    },
21    getValue() { return count; },
22    getHistory() { return [...history]; }
23  };
24}
25
26const escapeCounter = createCounter(0, 1);
27escapeCounter.increment(); // 1
28escapeCounter.increment(); // 2
29escapeCounter.increment(); // 3
30escapeCounter.decrement(); // 2
31
32console.log(escapeCounter.getValue());  // 2
33console.log(escapeCounter.getHistory()); // [1, 2, 3, 2]

Function Factories

Closures let you create factories that produce specialized functions:

1// Factory creating validators for various types
2function createRangeValidator(min, max, label) {
3  return function(value) {
4    if (value < min || value > max) {
5      throw new RangeError(`${label} must be between ${min} and ${max}, got ${value}`);
6    }
7    return true;
8  };
9}
10
11const validateHealth = createRangeValidator(0, 100, 'Health');
12const validateHunger = createRangeValidator(0, 100, 'Hunger');
13const validateWeight = createRangeValidator(1, 100000, 'Weight');
14
15validateHealth(85);   // OK
16validateHunger(60);   // OK
17// validateWeight(0); // RangeError: Weight must be between 1 and 100000, got 0
18
19// Multiplier factory
20function createMultiplier(factor) {
21  return n => n * factor;
22}
23
24const double = createMultiplier(2);
25const triple = createMultiplier(3);
26
27console.log(double(5));  // 10
28console.log(triple(4));  // 12

IIFE - Immediately Invoked Function Expression

IIFE creates an isolated scope without polluting the global namespace:

1// Without IIFE - pollutes global namespace
2var alertLevel = 0;
3var incidents = [];
4
5// With IIFE - everything private
6const parkAlertSystem = (function() {
7  let alertLevel = 0;  // private
8  let incidents = [];  // private
9
10  function escalate(type) {
11    const levels = { MINOR: 1, WARNING: 2, DANGER: 3, CRITICAL: 5 };
12    const newLevel = levels[type] || 1;
13    alertLevel = Math.max(alertLevel, newLevel);
14    incidents.push({ type, level: newLevel, time: Date.now() });
15    console.log(`Alert level now: ${alertLevel}`);
16  }
17
18  return {
19    raiseAlert: escalate,
20    getLevel: () => alertLevel,
21    getIncidents: () => [...incidents],
22    reset() { alertLevel = 0; incidents = []; }
23  };
24})();
25
26parkAlertSystem.raiseAlert('WARNING'); // Alert level now: 2
27parkAlertSystem.raiseAlert('CRITICAL'); // Alert level now: 5
28console.log(parkAlertSystem.getLevel()); // 5

Hoisting Revisited: var vs let

The classic

var
loop bug and how closures fix it:

1// BUG with var
2const funcs = [];
3for (var i = 0; i < 3; i++) {
4  funcs.push(function() {
5    return i; // captures the variable, not the value!
6  });
7}
8
9console.log(funcs[0]()); // 3 - not 0!
10console.log(funcs[1]()); // 3 - not 1!
11console.log(funcs[2]()); // 3 - not 2!
12// All see i=3 because var is function-scoped
13
14// FIX 1: Use let (block-scoped - each iteration gets its own i)
15const correctFuncs = [];
16for (let j = 0; j < 3; j++) {
17  correctFuncs.push(function() {
18    return j; // each closure has its own j
19  });
20}
21
22console.log(correctFuncs[0]()); // 0
23console.log(correctFuncs[1]()); // 1
24console.log(correctFuncs[2]()); // 2
25
26// FIX 2: Closure with IIFE (older approach)
27const iifeFuncs = [];
28for (var k = 0; k < 3; k++) {
29  iifeFuncs.push((function(captured) {
30    return function() { return captured; };
31  })(k));
32}
33
34console.log(iifeFuncs[0]()); // 0
35console.log(iifeFuncs[1]()); // 1
36console.log(iifeFuncs[2]()); // 2

"Closures are like the park's secret access codes" - says Dr. Rex. "Each code (closure) carries the exact context it was created in - the corridor, the time, the access level. Even after the person who created it has left, the code still works in the right context!"

Go to CodeWorlds