Callbacks, Module, Cascade, Curry and Memoization in JavaScript
Five patterns and techniques built on top of JavaScript's first-class functions and closures. Each solves a specific problem in how code is structured and how computation is managed.
These five concepts are patterns and techniques built on top of JavaScript’s first-class functions and closures. Each solves a specific problem in how code is structured and how computation is managed.
Callbacks
A callback is a function passed as an argument to another function, to be executed after that function completes its work. Callbacks are the foundational mechanism for handling asynchronous operations in JavaScript.
function fetchData(callback) {
setTimeout(function() {
const data = { user: 'Alice' };
callback(data);
}, 1000);
}
fetchData(function(result) {
console.log(result.user); // 'Alice'
});
The function passed to fetchData is the callback. It is invoked by fetchData once the asynchronous operation, represented here by setTimeout, completes. The calling function decides when the callback runs, not the caller.
Callbacks are also used in synchronous contexts, such as the array methods map, filter and forEach, where the callback is applied to each element of the array.
Module Pattern
The module pattern uses a closure to create a private scope. It exposes only a deliberate public interface while keeping internal variables and functions inaccessible from outside.
This is implemented using an Immediately Invoked Function Expression, or IIFE: a function that is defined and called in the same expression.
const counter = (function() {
let count = 0; // private
return {
increment() { count++; },
decrement() { count--; },
value() { return count; }
};
})();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
console.log(counter.count); // undefined
count is private. The returned object is the public interface. Nothing outside the IIFE can read or modify count directly. ES6 modules with import and export solve the same problem at the language level, but the module pattern remains relevant in codebases that do not use a module system.
Cascade
Cascading allows multiple methods to be called on the same object in sequence, in a single statement. This is achieved by having each method return this, the reference to the object itself, instead of undefined.
As described in Douglas Crockford’s JavaScript: The Good Parts, if methods return this instead of nothing, cascades become possible.
class Builder {
constructor() {
this.result = {};
}
setName(name) {
this.result.name = name;
return this;
}
setAge(age) {
this.result.age = age;
return this;
}
build() {
return this.result;
}
}
const person = new Builder()
.setName('Alice')
.setAge(30)
.build();
console.log(person); // { name: 'Alice', age: 30 }
Each method call returns this, allowing the next method to be called on the same object immediately. This style is also known as method chaining.
Curry
Currying transforms a function that takes multiple arguments into a sequence of functions that each take a single argument. Each call in the sequence returns a new function that waits for the next argument, until all arguments have been received and the final result is produced.
Curry works by creating a closure that holds the value of the current call and makes it available to the next.
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
multiply(2) returns a new function with a fixed at 2. That returned function can be stored, passed around, and called later with the remaining argument. Currying enables partial application: creating a specialised function from a general one by pre-filling some of its arguments.
Memoization
Memoization is an optimisation technique where a function stores the results of previous calls and returns the cached result when the same input is given again. According to Douglas Crockford’s JavaScript: The Good Parts, functions can use objects to remember the results of previous operations, making it possible to avoid unnecessary work.
function memoize(fn) {
const cache = {};
return function(n) {
if (n in cache) {
return cache[n];
}
cache[n] = fn(n);
return cache[n];
};
}
function slowSquare(n) {
return n * n;
}
const fastSquare = memoize(slowSquare);
console.log(fastSquare(4)); // computed: 16
console.log(fastSquare(4)); // returned from cache: 16
The cache object is closed over by the returned function. It persists between calls. On the first call with a given argument, the result is computed and stored. On subsequent calls with the same argument, the cached value is returned immediately.
Memoization is most valuable for functions that are computationally expensive and called repeatedly with the same inputs, such as recursive algorithms or complex data transformations.
What to Do Now
Implement a memoized Fibonacci function and observe how caching eliminates redundant recursive calls:
const fib = memoize(function(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(10)); // 55
console.log(fib(10)); // 55, returned from cache
Without memoization, computing fib(40) requires over one billion recursive calls. With memoization, it requires forty.