JavaScript and TypeScript course Β· Module 12: Functional Programming

Closures in Functional Programming

7 min read
In this lesson4

An incident counter kept in a global variable can be changed by any piece of code: the report script, an old security module, an intern's typo. When the counter shows nonsense, nobody knows whom to ask. We need a variable that only selected functions can access.

In Jurassic Park, each enclosure has its own security system - fence voltage, alert level, incident counter. This data is private and inaccessible from outside, but the functions managing the enclosure have full access to it. This is exactly how closures work - they create a private environment for data, accessible only through specific functions.

What is a Closure?

A closure is a function that "remembers" variables from the scope in which it was created, even after that scope has ceased to exist.

In JavaScript, every function remembers its lexical environment at the moment it is created, that is, the variables visible at the place where it was written in the code. The simplest example is a counter:

1function createCounter() {
2  let count = 0; // Private variable - inaccessible from outside
3
4  return {
5    increment() { count++; return count; },
6    decrement() { count--; return count; },
7    getCount() { return count; },
8  };
9}
10
11const counter = createCounter();
12counter.increment(); // 1
13counter.increment(); // 2
14counter.getCount();  // 2
15// count is completely private - there is no direct access to it

The createCounter call finished long ago, yet count is still alive, because the three methods hold a reference to it. counter.count returns undefined, since the variable is not a property of the object but part of the closure. A closure remembers the variable itself, not a copy of its value, which is why the second increment sees the result of the first. The counter's methods are not pure, because they change count, but that change is locked away in one place.

Data Privacy Through Closures

Closures allow you to create truly private variables in JavaScript - this is a key pattern in functional programming. Let's build an enclosure security system in two steps. First the inside of the safe, meaning the private state and a helper function:

1function createSecuritySystem(zoneName) {
2  let fenceVoltage = 10000;
3  let alertLevel = 0;
4  let incidentLog = [];
5
6  // Private helper function
7  function logEvent(event) {
8    incidentLog.push({ event, time: Date.now(), zone: zoneName });
9  }
10
11  // ...continued in the next block

The three let variables and the logEvent function exist only inside a single call of the factory. We do not return them, so from the outside they can be neither read nor changed.

Only an object with three methods goes out. It is the one and only door to the safe:

1  return {
2    raiseFence() {
3      fenceVoltage += 2000;
4      logEvent('Fence voltage raised to ' + fenceVoltage);
5      return fenceVoltage;
6    },
7    triggerAlert() {
8      alertLevel++;
9      logEvent('Alert level: ' + alertLevel);
10      return alertLevel;
11    },
12    getStatus() {
13      return {
14        zone: zoneName,
15        voltage: fenceVoltage,
16        alert: alertLevel,
17        incidents: incidentLog.length,
18      };
19    },
20  };
21}

Every method sees the private variables and logEvent, but decides for itself what to reveal: getStatus gives the number of incidents, not the log array itself. Let's check whether two enclosures really have separate safes:

1const zoneA = createSecuritySystem('Zone A');
2const zoneB = createSecuritySystem('Zone B');
3
4zoneA.raiseFence();  // 12000
5zoneA.triggerAlert(); // 1
6zoneB.getStatus();   // { zone: 'Zone B', voltage: 10000, alert: 0, incidents: 0 }
7// Each zone has its own isolated data

zoneA raised the voltage and sounded an alert, and zoneB knows nothing about it, because each call of the factory creates a new, independent closure. Watch out for one loophole: if a method returned the incidentLog array itself, outside code would get a reference to the original and could append to it with push. Return a copy, for example [...incidentLog]. Classes have private fields marked with #, which you know from the module on classes, and closures give the same effect without classes.

Function Factories

Closures are ideal for creating factories - functions that produce specialized functions. The smallest factory takes a threshold and returns a predicate that remembers it:

1function createFilter(minValue) {
2  return (item) => item.value >= minValue;
3}
4
5const isStrongSignal = createFilter(70);
6isStrongSignal({ value: 85 }); // true
7isStrongSignal({ value: 12 }); // false

Each call of createFilter gives a new filter with its own threshold, ready to be passed to the filter method. A factory can also lock a whole set of rules inside. Object.entries turns the rules object into an array of [field, rule] pairs, which the for...of loop walks through:

1// Validator factory
2function createValidator(rules) {
3  return function validate(data) {
4    const errors = [];
5    for (const [field, rule] of Object.entries(rules)) {
6      const value = data[field];
7      if (rule.required && (value === undefined || value === '')) {
8        errors.push(field + ' is required');
9      }
10      if (rule.min !== undefined && value < rule.min) {
11        errors.push(field + ' must be >= ' + rule.min);
12      }
13      if (rule.max !== undefined && value > rule.max) {
14        errors.push(field + ' must be <= ' + rule.max);
15      }
16    }
17    return { valid: errors.length === 0, errors };
18  };
19}
20
21const validateDinosaur = createValidator({
22  name: { required: true },
23  weight: { required: true, min: 1, max: 100000 },
24  health: { required: true, min: 0, max: 100 },
25});
26
27validateDinosaur({ name: 'Rex', weight: 8000, health: 95 });
28// { valid: true, errors: [] }
29
30validateDinosaur({ name: '', weight: -5, health: 150 });
31// { valid: false, errors: ['name is required', 'weight must be >= 1', 'health must be <= 100'] }

Notice the comparisons with undefined instead of a shortcut like rule.min && .... The rule min: 0 is a falsy value, so the shortcut would silently skip it, and the condition !value would treat a health of 0 as missing data. Inside validate there is a push, but only on the local errors array, so from the outside the function stays pure.

Memoization with Closures

Memoization is a technique for caching results of expensive computations. Closures provide a private cache. The key in the Map is the text from JSON.stringify(args), because two different argument arrays are never equal as objects:

1function memoize(fn) {
2  const cache = new Map();
3
4  return function(...args) {
5    const key = JSON.stringify(args);
6    if (cache.has(key)) {
7      console.log('Cache hit for:', key);
8      return cache.get(key);
9    }
10    const result = fn(...args);
11    cache.set(key, result);
12    return result;
13  };
14}

Now let's test memoization on a DNA sample analysis that simulates an expensive computation with a loop of a hundred thousand steps:

1// Expensive DNA analysis
2function analyzeDNA(sampleId, depth) {
3  // Simulating a long computation
4  let result = 0;
5  for (let i = 0; i < depth * 1000; i++) {
6    result += Math.sqrt(i);
7  }
8  return { sampleId, score: result / (depth * 1000) };
9}
10
11const memoizedAnalyze = memoize(analyzeDNA);
12memoizedAnalyze('TREX-001', 100); // computes
13memoizedAnalyze('TREX-001', 100); // from cache - instant!

The second call prints Cache hit for: ["TREX-001",100] and hands back the remembered result without computing. Memoize pure functions only: if the analysis drew its result with Math.random(), the cache would freeze the first draw and every later call would lie. That is why the simulation here uses the deterministic Math.sqrt.

In real code you will also meet a shorter version built on a plain object and the ||= operator (logical OR assignment from ES2021):

1const memoize = (fn) => {
2  const cache = {};
3  return (...args) => cache[args] ||= fn(...args);
4};

cache[args] turns the array of arguments into text, e.g. 'TREX-001,100', and ||= assigns the result only when the value under that key is falsy. The downside: a result equal to 0, '' or false will be computed from scratch every time. That is why I recommend the version with Map - it is longer, but free of this trap. Closures also power the currying from the previous lesson, and in the next one you will meet recursion.

Remember: a closure is an enclosure's safe - a function carries with it the key to the variables of the place where it was born, and the rest of the park sees only the doors you install yourself.

Code for this lesson: index.js
1// Closures in functional programming
2console.log("=== Jurassic Park - Private Security Systems ===\n");
3
4// --- EXAMPLE 1: Data privacy ---
5console.log("--- Data privacy ---");
6
7function createSecuritySystem(zoneName) {
8  let fenceVoltage = 10000;
9  let alertLevel = 0;
10  const incidentLog = [];
11
12  function logEvent(event) {
13    incidentLog.push({ event, zone: zoneName, time: Date.now() });
14  }
15
16  return {
17    raiseFence() {
18      fenceVoltage += 2000;
19      logEvent("Fence raised to " + fenceVoltage);
20      return fenceVoltage;
21    },
22    triggerAlert() {
23      alertLevel++;
24      logEvent("Alert: " + alertLevel);
25      return alertLevel;
26    },
27    getStatus() {
28      return {
29        zone: zoneName,
30        voltage: fenceVoltage,
31        alert: alertLevel,
32        incidents: incidentLog.length,
33      };
34    },
35  };
36}
37
38const zoneA = createSecuritySystem("Zone A");
39const zoneB = createSecuritySystem("Zone B");
40
41zoneA.raiseFence();
42zoneA.raiseFence();
43zoneA.triggerAlert();
44
45console.log("Zone A:", zoneA.getStatus());
46console.log("Zone B:", zoneB.getStatus());
47console.log("Each zone has isolated data!\n");
48
49// --- EXAMPLE 2: A validator factory ---
50console.log("--- Validator factory ---");
51
52function createValidator(rules) {
53  return function validate(data) {
54    const errors = [];
55    for (const [field, rule] of Object.entries(rules)) {
56      if (rule.required && !data[field]) {
57        errors.push(field + " is required");
58      }
59      if (rule.min !== undefined && data[field] < rule.min) {
60        errors.push(field + " must be >= " + rule.min);
61      }
62      if (rule.max !== undefined && data[field] > rule.max) {
63        errors.push(field + " must be <= " + rule.max);
64      }
65    }
66    return { valid: errors.length === 0, errors };
67  };
68}
69
70const validateDino = createValidator({
71  name: { required: true },
72  weight: { required: true, min: 1, max: 100000 },
73  health: { required: true, min: 0, max: 100 },
74});
75
76console.log("Valid:", validateDino({ name: "Rex", weight: 8000, health: 95 }));
77console.log("Invalid:", validateDino({ name: "", weight: -5, health: 150 }));
78
79// --- EXAMPLE 3: Memoization ---
80console.log("\n--- Memoization ---");
81
82function memoize(fn) {
83  const cache = new Map();
84  return function(...args) {
85    const key = JSON.stringify(args);
86    if (cache.has(key)) {
87      console.log("  Cache HIT:", key);
88      return cache.get(key);
89    }
90    console.log("  Computing:", key);
91    const result = fn(...args);
92    cache.set(key, result);
93    return result;
94  };
95}
96
97function expensiveAnalysis(species, depth) {
98  let sum = 0;
99  for (let i = 0; i < depth * 100; i++) sum += i;
100  return { species, score: sum };
101}
102
103const memoAnalysis = memoize(expensiveAnalysis);
104memoAnalysis("T-Rex", 10);     // Computing
105memoAnalysis("T-Rex", 10);     // Cache HIT
106memoAnalysis("Raptor", 5);     // Computing
107memoAnalysis("Raptor", 5);     // Cache HIT
108memoAnalysis("T-Rex", 10);     // Cache HIT

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. 1. What is a closure in JavaScript?

  2. 2. What is memoization in the context of functional programming?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Build an incident tracker factory with private data and a memoize function.

  • Horizontal ordering

    Arrange the elements of a function factory with closure:

  • Code editor

    Rewrite imperative for loops using filter, map, and reduce with pipe.

Useful articles