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.
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 directlyClosures 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]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)); // 12IIFE 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()); // 5The 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!"