JavaScript Closures Tutorial - Section 2: Practical Patterns
Function composition is the process of combining simple functions to build more complex ones. It's like connecting pipes where the output of one function becomes the input of the next.
// Simple functions
const add5 = x => x + 5;
const multiply3 = x => x * 3;
const subtract2 = x => x - 2;
// Manual composition
const result = subtract2(multiply3(add5(10)));
console.log(result); // 43
// Steps: 10 → 15 → 45 → 43
Create a utility that composes functions right-to-left:
function compose(...fns) {
return function(value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
// Use it
const calculate = compose(
subtract2,
multiply3,
add5
);
console.log(calculate(10)); // 43
// Reads right-to-left: add5 → multiply3 → subtract2
Create a utility that composes functions left-to-right (more intuitive):
function pipe(...fns) {
return function(value) {
return fns.reduce((acc, fn) => fn(acc), value);
};
}
// Use it
const calculate = pipe(
add5,
multiply3,
subtract2
);
console.log(calculate(10)); // 43
// Reads left-to-right: add5 → multiply3 → subtract2
// Data transformation functions
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s+/g, '-');
const addPrefix = prefix => str => `${prefix}${str}`;
const addSuffix = suffix => str => `${str}${suffix}`;
// Create a slug generator
const createSlug = pipe(
trim,
toLowerCase,
removeSpaces,
addPrefix('blog-'),
addSuffix('-2024')
);
console.log(createSlug(' Hello World '));
// Output: "blog-hello-world-2024"
// Validation functions
const isNotEmpty = value => {
if (!value || value.trim() === '') {
throw new Error('Value cannot be empty');
}
return value;
};
const isEmail = value => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error('Invalid email format');
}
return value;
};
const isLongEnough = min => value => {
if (value.length < min) {
throw new Error(`Value must be at least ${min} characters`);
}
return value;
};
const normalizeEmail = value => value.toLowerCase().trim();
// Create email validator
const validateEmail = pipe(
isNotEmpty,
normalizeEmail,
isLongEnough(5),
isEmail
);
try {
const email = validateEmail(' Alice@Example.COM ');
console.log('Valid email:', email); // "alice@example.com"
} catch (error) {
console.error(error.message);
}
// Array transformation functions
const filterAdults = users => users.filter(u => u.age >= 18);
const sortByAge = users => [...users].sort((a, b) => a.age - b.age);
const extractNames = users => users.map(u => u.name);
const joinWithComma = names => names.join(', ');
// Create user processor
const processUsers = pipe(
filterAdults,
sortByAge,
extractNames,
joinWithComma
);
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 17 },
{ name: 'Charlie', age: 30 },
{ name: 'David', age: 16 },
{ name: 'Eve', age: 22 }
];
console.log(processUsers(users));
// Output: "Eve, Alice, Charlie"
Closures make composition even more powerful by allowing configuration:
// Configurable transformations
const multiply = factor => value => value * factor;
const add = amount => value => value + amount;
const power = exponent => value => Math.pow(value, exponent);
const round = decimals => value => {
const multiplier = Math.pow(10, decimals);
return Math.round(value * multiplier) / multiplier;
};
// Create specialized calculators
const calculatePrice = pipe(
multiply(1.2), // Add 20% markup
add(5), // Add $5 shipping
multiply(1.08), // Add 8% tax
round(2) // Round to 2 decimals
);
console.log(calculatePrice(100)); // 133.60
// Create different calculator
const calculateDiscount = pipe(
multiply(0.8), // 20% discount
round(2)
);
console.log(calculateDiscount(100)); // 80.00
Compose asynchronous functions:
function pipeAsync(...fns) {
return async function(value) {
let result = value;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
}
// Async transformation functions
const fetchUser = async id => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const enrichUserData = async user => {
const orders = await fetch(`/api/orders?userId=${user.id}`).then(r => r.json());
return { ...user, orders };
};
const calculateTotalSpent = user => {
const total = user.orders.reduce((sum, order) => sum + order.total, 0);
return { ...user, totalSpent: total };
};
const formatUserReport = user => ({
name: user.name,
email: user.email,
orderCount: user.orders.length,
totalSpent: `$${user.totalSpent.toFixed(2)}`
});
// Create user report generator
const generateUserReport = pipeAsync(
fetchUser,
enrichUserData,
calculateTotalSpent,
formatUserReport
);
// Use it
generateUserReport(123).then(report => console.log(report));
// Middleware functions
const logger = next => req => {
console.log(`${req.method} ${req.url}`);
return next(req);
};
const auth = next => req => {
if (!req.headers.authorization) {
throw new Error('Unauthorized');
}
req.user = { id: 123, name: 'Alice' };
return next(req);
};
const parseBody = next => req => {
if (req.body) {
req.parsedBody = JSON.parse(req.body);
}
return next(req);
};
const rateLimit = limit => next => {
let count = 0;
return req => {
count++;
if (count > limit) {
throw new Error('Rate limit exceeded');
}
return next(req);
};
};
// Final handler
const handler = req => {
return {
status: 200,
body: { message: 'Success', user: req.user }
};
};
// Compose middleware
function composeMiddleware(...middlewares) {
return middlewares.reduceRight(
(next, middleware) => middleware(next),
handler
);
}
const app = composeMiddleware(
logger,
auth,
parseBody,
rateLimit(100)
);
// Use it
const request = {
method: 'POST',
url: '/api/data',
headers: { authorization: 'Bearer token' },
body: '{"name":"test"}'
};
try {
const response = app(request);
console.log(response);
} catch (error) {
console.error(error.message);
}
// Event processors
const validateEvent = event => {
if (!event.type || !event.data) {
throw new Error('Invalid event format');
}
return event;
};
const enrichEvent = event => ({
...event,
timestamp: Date.now(),
id: Math.random().toString(36).substr(2, 9)
});
const filterByType = types => event => {
if (!types.includes(event.type)) {
throw new Error(`Event type ${event.type} not allowed`);
}
return event;
};
const logEvent = event => {
console.log(`[${event.timestamp}] ${event.type}:`, event.data);
return event;
};
const saveEvent = event => {
// Simulate saving to database
console.log('Saved event:', event.id);
return event;
};
// Create event processor
const processEvent = pipe(
validateEvent,
enrichEvent,
filterByType(['user.created', 'user.updated', 'user.deleted']),
logEvent,
saveEvent
);
// Use it
try {
processEvent({
type: 'user.created',
data: { name: 'Alice', email: 'alice@example.com' }
});
} catch (error) {
console.error('Event processing failed:', error.message);
}
compose reads right-to-left, pipe reads left-to-rightYou've completed Section 2 on Practical Patterns! In the next section, we'll explore common use cases for closures, starting with event handlers and callbacks.