JavaScript and TypeScript course Β· Module 2: Functions, Arrays and Objects
Closures and Lexical Scope
In this lesson7
In Jurassic Park every paddock has a fence and every lab has a door with an access code. Scope in JavaScript works the same way: it is an invisible fence that decides which variables are available in a given place in the code. There is also a problem: how do you build a sighting counter that no other system can reset by mistake? A global variable is open to everyone. The answer is closures, a dinosaur that remembers the PIN to its paddock long after the keeper has gone.
Lexical Scope
JavaScript uses lexical scope: access to variables depends on where the code was written, not on where it was called. Picture the park map: each paddock (function) sees the main road (global scope) and its own territory, but it cannot see inside other paddocks.
1// Global scope - the main road of Jurassic Park
2const parkName = "Jurassic Park";
3
4function enclosureA() {
5 // Scope of enclosureA - Paddock A
6 const dinosaurA = "Tyrannosaurus Rex";
7
8 console.log(parkName); // "Jurassic Park" - sees the global scope
9 console.log(dinosaurA); // "Tyrannosaurus Rex" - sees its own scope
10
11 function innerFence() {
12 // Nested scope - the inner fence
13 const babyDino = "Baby Rex";
14
15 console.log(parkName); // "Jurassic Park" - sees the global scope
16 console.log(dinosaurA); // "Tyrannosaurus Rex" - sees the parent scope
17 console.log(babyDino); // "Baby Rex" - sees its own scope
18 }
19
20 innerFence();
21 // console.log(babyDino); // ERROR! Cannot see inside innerFence
22}
23
24function enclosureB() {
25 // Scope of enclosureB - Paddock B
26 const dinosaurB = "Triceratops";
27
28 console.log(parkName); // "Jurassic Park" - sees the global scope
29 // console.log(dinosaurA); // ERROR! Cannot see inside enclosureA
30}The rule is simple: an inner function sees the variables of the outer function, but not the other way round. A dinosaur in the inner paddock can see out through the fence, while a keeper outside cannot look through the double walls. That is why innerFence reads dinosaurA without receiving it as an argument, and enclosureB has no access to it.
What Is a Closure?
A closure is a function that "remembers" the variables from the scope where it was created, even after that scope has finished running. See it in a sighting tracker:
1function createDinoTracker(species) {
2 // The "species" variable lives in the scope of createDinoTracker
3 let sightings = 0;
4
5 // This inner function creates a CLOSURE
6 // - it "remembers" species and sightings, even after createDinoTracker ends
7 return function reportSighting(location) {
8 sightings++;
9 console.log(species + " seen at: " + location + " (sighting #" + sightings + ")");
10 };
11}
12
13// createDinoTracker has finished, but the returned function REMEMBERS the variables
14const trackRex = createDinoTracker("T-Rex");
15const trackRaptor = createDinoTracker("Velociraptor");
16
17trackRex("Sector A"); // "T-Rex seen at: Sector A (sighting #1)"
18trackRex("Sector B"); // "T-Rex seen at: Sector B (sighting #2)"
19trackRaptor("Laboratory"); // "Velociraptor seen at: Laboratory (sighting #1)"
20
21// Each closure has its OWN copy of the variables!
22// trackRex has its own sightings = 2, trackRaptor has its own sightings = 1Each call to createDinoTracker creates a new scope, so the counters of the two trackers are independent. Nobody outside can change sightings, because the variable is not visible outside the function. The mechanism follows five steps:
- You define an outer function with local variables.
- Inside it, you define an inner function that uses them.
- The outer function returns the inner function with
return. - You call the outer function and store the result in a variable.
- You call the returned function, and it still has access to the outer variables.
Practical Uses of Closures
Data Privacy (Encapsulation)
Closures let you create private variables that nobody outside can reach. Like the park security system, where the access codes are locked away in a sealed cabinet:
1function createSecuritySystem(enclosureName) {
2 // These variables are PRIVATE - inaccessible from outside
3 let accessCode = Math.floor(Math.random() * 9000) + 1000;
4 let isLocked = true;
5 const accessLog = [];
6
7 return {
8 unlock(code) {
9 if (code === accessCode) {
10 isLocked = false;
11 accessLog.push("Unlocked: " + new Date().toLocaleTimeString());
12 console.log(enclosureName + " - fence DISABLED");
13 } else {
14 accessLog.push("Failed attempt: " + new Date().toLocaleTimeString());
15 console.log(enclosureName + " - WRONG CODE!");
16 }
17 },
18 lock() {
19 isLocked = true;
20 accessLog.push("Locked: " + new Date().toLocaleTimeString());
21 console.log(enclosureName + " - fence ENABLED");
22 },
23 getStatus() {
24 return isLocked ? "LOCKED" : "OPEN";
25 },
26 getLog() {
27 return [...accessLog]; // Returns a copy, not the original
28 }
29 };
30}
31
32const rexSecurity = createSecuritySystem("T-Rex Paddock");
33// There is no way to read accessCode directly!
34// rexSecurity.accessCode β undefined (private variable)The returned object has only four methods. accessCode, isLocked and accessLog are not its properties but variables from the closure, so only those methods can change them. getLog() hands out a copy of the array, so clearing the result will not touch the real log.
Function Factories
Closures make it possible to build factories, functions that produce other functions with built-in configuration. Like a genetics lab that creates dinosaurs from specific DNA:
1function createDinoFilter(property, value) {
2 // The closure "remembers" property and value
3 return function(dinosaur) {
4 return dinosaur[property] === value;
5 };
6}
7
8const isCarnivore = createDinoFilter("diet", "carnivore");
9const isHerbivore = createDinoFilter("diet", "herbivore");
10const isSectorA = createDinoFilter("sector", "A");
11
12const dinosaurs = [
13 { name: "Rex", diet: "carnivore", sector: "A" },
14 { name: "Tricia", diet: "herbivore", sector: "B" },
15 { name: "Blue", diet: "carnivore", sector: "C" }
16];
17
18console.log(dinosaurs.filter(isCarnivore)); // Rex, Blue
19console.log(dinosaurs.filter(isHerbivore)); // Tricia
20console.log(dinosaurs.filter(isSectorA)); // RexThree filters came out of one factory, and each remembers its own property and value pair. The smallest factory fits in three lines:
1function createMultiplier(factor) {
2 return n => n * factor;
3}
4
5const double = createMultiplier(2);
6const triple = createMultiplier(3);
7
8console.log(double(5)); // 10
9console.log(triple(4)); // 12The arrow function remembers factor, so double and triple share the same code but have different configurations.
IIFE - Immediately Invoked Function Expressions
An IIFE (immediately invoked function expression) is a function that is defined and run right away. It is a one-time security protocol: it starts, does its job and disappears without leaving traces in the global scope.
1// IIFE syntax
2(function() {
3 const secretData = "Location of the hidden lab";
4 console.log("IIFE executed! Data: " + secretData);
5})();
6// secretData does not exist in the global scope!
7
8// Arrow IIFE
9(() => {
10 const emergencyProtocol = "Evacuate sector 7";
11 console.log("Emergency protocol activated: " + emergencyProtocol);
12})();
13
14// IIFE with parameters
15const dinoReport = (function(parkData) {
16 const processed = parkData.map(d => d.toUpperCase());
17 return processed.join(", ");
18})(["rex", "raptor", "triceratops"]);
19
20console.log(dinoReport); // "REX, RAPTOR, TRICERATOPS"The structure is always the same: an opening parenthesis, function(), the body { /* code */ }, a closing parenthesis and (), which calls the function. The parentheses around the function turn it into an expression; without them the engine reports a syntax error. When an IIFE returns an object with methods, you get a module with private state:
1const visitorCounter = (function() {
2 let count = 0; // private state of the module
3
4 return {
5 increment() { return ++count; },
6 getValue() { return count; }
7 };
8})();
9
10visitorCounter.increment(); // 1
11visitorCounter.increment(); // 2
12console.log(visitorCounter.getValue()); // 2IIFEs were very popular before ES6, when var had no block scope. They still isolate code today, but in a new project I recommend ES modules with import and export, and it is worth recognizing IIFEs in older code.
Hoisting - var vs let/const
Hoisting is the mechanism in which variable and function declarations are "moved" to the top of their scope. Each keyword behaves differently, though:
1// var - hoisted with the value undefined
2console.log(dinoVar); // undefined (not an error!)
3var dinoVar = "Stegosaurus";
4console.log(dinoVar); // "Stegosaurus"
5
6// let/const - hoisted, but with a "Temporal Dead Zone" (TDZ)
7// console.log(dinoLet); // ReferenceError! The variable is in the TDZ
8let dinoLet = "Ankylosaurus";
9console.log(dinoLet); // "Ankylosaurus"
10
11// const - same as let, plus it requires immediate initialization
12// console.log(dinoConst); // ReferenceError! TDZ
13const dinoConst = "Brachiosaurus";A let variable exists from the start of the block, but until its declaration line it sits in a no-entry zone, so reading it ends with a ReferenceError, not with undefined or null.
var vs let in Loops - the Classic Trap
This is one of the most famous JavaScript problems, where closures and var produce unexpected results:
1// The problem with var - all closures share the same variable
2for (var i = 0; i < 3; i++) {
3 setTimeout(function() {
4 console.log("var: Dinosaur #" + i); // Always "Dinosaur #3"!
5 }, 100);
6}
7
8// The fix with let - each iteration gets its own copy
9for (let j = 0; j < 3; j++) {
10 setTimeout(function() {
11 console.log("let: Dinosaur #" + j); // 0, 1, 2 - correct!
12 }, 100);
13}Why does var fail? It has function scope, so all three closures refer to one variable i, which equals 3 after the loop. let has block scope, and each iteration creates a new variable j. Before let existed, developers used an IIFE that freezes the current value:
1// The older fix: an IIFE captures the current value
2for (var k = 0; k < 3; k++) {
3 (function(captured) {
4 setTimeout(function() {
5 console.log("IIFE: Dinosaur #" + captured); // 0, 1, 2
6 }, 100);
7 })(k);
8}It works, but let does the same without an extra function, so choose let.
Summary
| Concept | Jurassic Park Analogy |
|---|---|
| Global scope | The main park road - visible from every paddock |
| Function scope | A dinosaur's paddock - it sees out, but nobody sees in |
| Closure | A dinosaur remembers the park layout even after the fences are rebuilt |
| Private variables | The fence PIN code - hidden inside a sealed system |
| IIFE | A one-time security protocol - run and forget |
| Hoisting var | A dinosaur that appears on the list before it hatches |
| TDZ (let/const) | A no-entry zone until the paddock officially opens |
Closures will come back in the module on asynchronous code, because every callback passed to setTimeout or a promise relies on a closure.
Practice
The sandbox contains lexical scope, the sighting tracker and a security system with a private log. Call the trackers a few times and check that each one counts separately.
Code for this lesson: index.js
1// Closures and lexical scope - Jurassic Park
2console.log("=== CLOSURES IN JURASSIC PARK ===\n");
3
4// ================================================
5// 1. LEXICAL SCOPE - park enclosures
6// ================================================
7console.log("--- 1. Lexical scope ---");
8
9const parkName = "Jurassic Park"; // Global scope
10
11function enclosureA() {
12 const dinosaurA = "Tyrannosaurus Rex"; // enclosureA scope
13
14 function innerFence() {
15 const babyDino = "Baby Rex"; // innerFence scope
16 // Sees EVERYTHING: its own variables + the parent's + global ones
17 console.log("innerFence sees: " + parkName + ", " + dinosaurA + ", " + babyDino);
18 }
19
20 innerFence();
21 // console.log(babyDino); // ERROR! Cannot see inside innerFence
22 console.log("enclosureA sees: " + parkName + ", " + dinosaurA);
23}
24
25enclosureA();
26
27// ================================================
28// 2. CLOSURE - the dinosaur remembers its environment
29// ================================================
30console.log("\n--- 2. Closure - dinosaur tracker ---");
31
32function createDinoTracker(species) {
33 let sightings = 0; // This variable lives in the closure!
34
35 return function reportSighting(location) {
36 sightings++; // The closure remembers and modifies sightings
37 console.log(species + " sighted at: " + location + " (observation #" + sightings + ")");
38 };
39}
40
41const trackRex = createDinoTracker("T-Rex");
42const trackRaptor = createDinoTracker("Velociraptor");
43
44trackRex("Sector A");
45trackRex("Sector B");
46trackRaptor("Laboratory");
47trackRex("Sector C");
48// Each tracker has its OWN copy of the sightings variable!
49
50// ================================================
51// 3. DATA PRIVACY - security system
52// ================================================
53console.log("\n--- 3. Private variables (encapsulation) ---");
54
55function createSecuritySystem(enclosureName) {
56 let isLocked = true; // PRIVATE variable
57 const log = [];
58
59 return {
60 unlock() {
61 isLocked = false;
62 log.push("Unlocked at " + new Date().toLocaleTimeString());
63 console.log(enclosureName + " - fence DISABLED");
64 },
65 lock() {
66 isLocked = true;
67 log.push("Locked at " + new Date().toLocaleTimeString());
68 console.log(enclosureName + " - fence ENABLED");
69 },
70 getStatus() {
71 return enclosureName + ": " + (isLocked ? "LOCKED" : "OPEN");
72 },
73 getLog() { return [...log]; }
74 };
75}
76
77const rexSecurity = createSecuritySystem("T-Rex Paddock");
78console.log(rexSecurity.getStatus());
79rexSecurity.unlock();
80console.log(rexSecurity.getStatus());
81rexSecurity.lock();
82console.log("Log:", rexSecurity.getLog());
83// rexSecurity.isLocked -> undefined (private!)
84
85// ================================================
86// 4. FACTORY FUNCTION
87// ================================================
88console.log("\n--- 4. Factory function ---");
89
90function createDinoFilter(property, value) {
91 return function(dinosaur) {
92 return dinosaur[property] === value;
93 };
94}
95
96const dinosaurs = [
97 { name: "Rex", diet: "carnivore", sector: "A" },
98 { name: "Tricia", diet: "herbivore", sector: "B" },
99 { name: "Blue", diet: "carnivore", sector: "C" },
100 { name: "Steggy", diet: "herbivore", sector: "A" }
101];
102
103const isCarnivore = createDinoFilter("diet", "carnivore");
104const isSectorA = createDinoFilter("sector", "A");
105
106console.log("Carnivores:", dinosaurs.filter(isCarnivore).map(d => d.name));
107console.log("Sector A:", dinosaurs.filter(isSectorA).map(d => d.name));
108
109// ================================================
110// 5. IIFE - a one-time protocol
111// ================================================
112console.log("\n--- 5. IIFE ---");
113
114const emergencyReport = (function(dinos) {
115 const carnivores = dinos.filter(d => d.diet === "carnivore");
116 const herbivores = dinos.filter(d => d.diet === "herbivore");
117 return "Report: " + carnivores.length + " carnivores, " + herbivores.length + " herbivores";
118})(dinosaurs);
119
120console.log(emergencyReport);
121
122// ================================================
123// 6. HOISTING - var vs let
124// ================================================
125console.log("\n--- 6. Hoisting ---");
126
127// var - hoisting with undefined
128console.log("var before declaration:", typeof dinoVar); // undefined
129var dinoVar = "Stegosaurus";
130console.log("var after declaration:", dinoVar);
131
132// let - Temporal Dead Zone
133// console.log(dinoLet); // ReferenceError!
134let dinoLet = "Ankylosaurus";
135console.log("let after declaration:", dinoLet);
136
137// var vs let in a loop
138console.log("\nLoop with let:");
139for (let j = 0; j < 3; j++) {
140 setTimeout(() => console.log(" let: Dinosaur #" + j), 10);
141}Remember: a closure is a dinosaur that remembers its paddock, a function that carries the variables from the place where it was born.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What will we see in the console after running the following code? function outer() { const x = 10; function inner() { console.log(x); } inner(); } outer();
2. What is a closure in JavaScript?
3. What will happen when we try to use a variable declared with let BEFORE its declaration? console.log(dinoName); let dinoName = "Rex";
Hands-on tasks in the game
- Code editor
Write a function createDinoTracker(species) that keeps a private counter sightings and an array history in a closure and returns an object with the methods: reportSighting(location) (increases the counter and adds the location to the history), getSightings() (returns the number of sightings) and getHistory() (returns a copy of the history). Also write createEnclosure(name, maxCapacity) with a private array residents and the methods: addDinosaur(dinoName) (adds the dinosaur and returns true, or returns false when there is no room), removeDinosaur(dinoName), getPopulation() (returns a copy of the array) and getStatus() (returns the text 'name: X/maxCapacity', e.g. 'Paddock T-Rex: 1/2').
- Vertical ordering
Arrange the steps of creating and using a closure in JavaScript:
- Click in order
Build a correct IIFE (Immediately Invoked Function Expression) by clicking elements in the right order:
- Code editor
Write a factory createFilter(property, value) that returns a function checking whether object[property] === value, and a factory createWeightConverter(multiplier) that returns a function multiplying a weight in kilograms by multiplier. Then use an IIFE to create a counter dinoCounter with a private variable count and the methods increment(), decrement() and getValue().