Closure Application Patterns: Best Practices in Real-world Scenarios
Overview of Closure Patterns
After mastering the basic concepts of closures, it's more important to understand how to apply closures in actual projects. Just like learning the technique of laying bricks, the next step is to learn how to build different types of buildings—from simple cottages to complex skyscrapers. This article will introduce commonly used closure application patterns in JavaScript development, all of which are time-tested and widely used best practices in real projects.
Module Pattern
The module pattern is one of the most common closure applications, using closures to create private scope and implement data encapsulation and information hiding.
Basic Module Pattern
const UserModule = (function () {
// Private variables
let users = [];
let currentId = 1;
// Private methods
function generateId() {
return currentId++;
}
function validateUser(user) {
if (!user.name || user.name.trim() === "") {
throw new Error("User must have a name");
}
if (!user.email || !isValidEmail(user.email)) {
throw new Error("User must have a valid email");
}
return true;
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Public API
return {
createUser(userData) {
const user = {
id: generateId(),
name: userData.name,
email: userData.email,
createdAt: new Date(),
};
validateUser(user);
users.push(user);
return user.id;
},
getUser(id) {
return users.find((user) => user.id === id);
},
updateUser(id, updates) {
const user = users.find((u) => u.id === id);
if (!user) {
throw new Error("User not found");
}
Object.assign(user, updates);
validateUser(user);
user.updatedAt = new Date();
return user;
},
deleteUser(id) {
const index = users.findIndex((u) => u.id === id);
if (index !== -1) {
users.splice(index, 1);
return true;
}
return false;
},
getAllUsers() {
return users.map((u) => ({ ...u })); // Return copy
},
};
})();
// Use module
const userId = UserModule.createUser({
name: "Sarah Johnson",
email: "[email protected]",
});
console.log(UserModule.getUser(userId));
// { id: 1, name: 'Sarah Johnson', email: '[email protected]', ... }
// Private members cannot be accessed
// console.log(users); // ReferenceError
// UserModule.generateId(); // TypeError: not a functionRevealing Module Pattern
This is a variant of the module pattern where all methods are defined in the private scope and then exposed uniformly:
const Calculator = (function () {
// Private state
let history = [];
let precision = 2;
// Private helper functions
function round(number) {
return Number(number.toFixed(precision));
}
function recordOperation(operation, operands, result) {
history.push({
operation,
operands,
result,
timestamp: new Date(),
});
}
// Public methods (all defined in private scope)
function add(a, b) {
const result = round(a + b);
recordOperation("add", [a, b], result);
return result;
}
function subtract(a, b) {
const result = round(a - b);
recordOperation("subtract", [a, b], result);
return result;
}
function multiply(a, b) {
const result = round(a * b);
recordOperation("multiply", [a, b], result);
return result;
}
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
const result = round(a / b);
recordOperation("divide", [a, b], result);
return result;
}
function getHistory() {
return [...history];
}
function clearHistory() {
history = [];
}
function setPrecision(value) {
if (value < 0 || value > 10) {
throw new Error("Precision must be between 0 and 10");
}
precision = value;
}
// Reveal public interface
return {
add,
subtract,
multiply,
divide,
getHistory,
clearHistory,
setPrecision,
};
})();
// Use
Calculator.add(10, 5); // 15
Calculator.multiply(3, 7); // 21
Calculator.divide(10, 3); // 3.33
console.log(Calculator.getHistory());
// [
// { operation: 'add', operands: [10, 5], result: 15, ... },
// { operation: 'multiply', operands: [3, 7], result: 21, ... },
// { operation: 'divide', operands: [10, 3], result: 3.33, ... }
// ]Module Enhancement and Extension
Modules can be enhanced by passing the module itself:
const TaskManager = (function () {
let tasks = [];
return {
addTask(task) {
tasks.push(task);
},
getTasks() {
return [...tasks];
},
};
})();
// Extend module
const TaskManager_Extended = (function (module) {
// Preserve original private state
let filterCache = new Map();
// Add new methods
module.filterTasks = function (predicate) {
const key = predicate.toString();
if (filterCache.has(key)) {
return filterCache.get(key);
}
const result = this.getTasks().filter(predicate);
filterCache.set(key, result);
return result;
};
module.clearCache = function () {
filterCache.clear();
};
return module;
})(TaskManager);
// Use extended module
TaskManager_Extended.addTask({ title: "Task 1", completed: false });
TaskManager_Extended.addTask({ title: "Task 2", completed: true });
const incompleteTasks = TaskManager_Extended.filterTasks((t) => !t.completed);
console.log(incompleteTasks); // [{ title: 'Task 1', completed: false }]Singleton Pattern
Using closures to ensure a class has only one instance:
const DatabaseConnection = (function () {
let instance;
function createConnection(config) {
// Private state
const connectionId = Math.random().toString(36).substr(2, 9);
const createdAt = new Date();
let isConnected = false;
let queryCount = 0;
// Private methods
function log(message) {
console.log(`[${connectionId}] ${message}`);
}
// Public interface
return {
connect() {
if (isConnected) {
log("Already connected");
return;
}
log(`Connecting to ${config.host}:${config.port}`);
isConnected = true;
},
disconnect() {
if (!isConnected) {
log("Already disconnected");
return;
}
log("Disconnecting...");
isConnected = false;
},
query(sql) {
if (!isConnected) {
throw new Error("Not connected to database");
}
queryCount++;
log(`Executing query #${queryCount}: ${sql}`);
// Simulate query
return { success: true, rows: [] };
},
getStats() {
return {
connectionId,
createdAt,
isConnected,
queryCount,
};
},
};
}
return {
getInstance(config) {
if (!instance) {
instance = createConnection(config);
}
return instance;
},
resetInstance() {
instance = null;
},
};
})();
// Use singleton
const db1 = DatabaseConnection.getInstance({
host: "localhost",
port: 5432,
});
const db2 = DatabaseConnection.getInstance({
host: "remotehost", // This config will be ignored
port: 3306,
});
console.log(db1 === db2); // true - same instance
db1.connect();
db1.query("SELECT * FROM users");
console.log(db1.getStats());
console.log(db2.getStats()); // Same statsFactory Pattern
Using closures to create object factories:
function createUserFactory(defaultRole) {
// Factory-level private state
let userCount = 0;
const createdUsers = [];
// Factory-level private methods
function generateUsername(name) {
const baseName = name.toLowerCase().replace(/\s+/g, "");
return `${baseName}_${Date.now()}`;
}
function trackUser(user) {
createdUsers.push({
id: user.id,
createdAt: new Date(),
});
}
// Return factory function
return function createUser(name, customRole) {
userCount++;
const user = {
id: userCount,
name,
username: generateUsername(name),
role: customRole || defaultRole,
// Instance methods (each user has them)
getProfile() {
return {
id: this.id,
name: this.name,
username: this.username,
role: this.role,
};
},
hasPermission(permission) {
const rolePermissions = {
admin: ["read", "write", "delete"],
user: ["read"],
guest: [],
};
return rolePermissions[this.role]?.includes(permission) || false;
},
};
trackUser(user);
return user;
};
}
// Create different factories
const createAdmin = createUserFactory("admin");
const createRegularUser = createUserFactory("user");
const admin = createAdmin("John Smith");
const user1 = createRegularUser("Sarah Wilson");
const user2 = createRegularUser("Michael Brown");
console.log(admin.hasPermission("delete")); // true
console.log(user1.hasPermission("delete")); // false
console.log(user2.hasPermission("read")); // trueMethod Chaining Pattern
Implement method chaining by returning the function itself:
function createQueryBuilder(tableName) {
// Private state
let query = {
table: tableName,
select: [],
where: [],
orderBy: [],
limit: null,
};
const builder = {
SELECT(...fields) {
query.select.push(...fields);
return this; // Return self for chaining
},
WHERE(condition) {
query.where.push(condition);
return this;
},
AND(condition) {
if (query.where.length === 0) {
throw new Error("Cannot use AND without WHERE");
}
query.where.push(`AND ${condition}`);
return this;
},
OR(condition) {
if (query.where.length === 0) {
throw new Error("Cannot use OR without WHERE");
}
query.where.push(`OR ${condition}`);
return this;
},
ORDER_BY(field, direction = "ASC") {
query.orderBy.push(`${field} ${direction}`);
return this;
},
LIMIT(count) {
query.limit = count;
return this;
},
build() {
let sql = "SELECT ";
sql += query.select.length > 0 ? query.select.join(", ") : "*";
sql += ` FROM ${query.table}`;
if (query.where.length > 0) {
sql += " WHERE " + query.where.join(" ");
}
if (query.orderBy.length > 0) {
sql += " ORDER BY " + query.orderBy.join(", ");
}
if (query.limit) {
sql += ` LIMIT ${query.limit}`;
}
return sql;
},
reset() {
query = {
table: tableName,
select: [],
where: [],
orderBy: [],
limit: null,
};
return this;
},
};
return builder;
}
// Use method chaining
const userQuery = createQueryBuilder("users")
.SELECT("id", "name", "email")
.WHERE("age > 18")
.AND('status = "active"')
.OR('role = "admin"')
.ORDER_BY("created_at", "DESC")
.LIMIT(10)
.build();
console.log(userQuery);
// SELECT id, name, email FROM users
// WHERE age > 18 AND status = "active" OR role = "admin"
// ORDER BY created_at DESC LIMIT 10Currying Pattern
Convert multi-parameter functions to a series of single-parameter functions:
// General currying function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
};
}
// Practical application examples
const log = curry(function (level, timestamp, message) {
console.log(`[${level}] ${timestamp}: ${message}`);
});
// Create specialized logging functions
const error = log("ERROR");
const errorNow = error(new Date().toISOString());
errorNow("Database connection failed");
errorNow("Invalid user input");
const info = log("INFO");
const infoNow = info(new Date().toISOString());
infoNow("User logged in");
infoNow("Task completed");
// Can also pass all parameters at once
log("WARN", new Date().toISOString(), "Cache is full");
// More practical example: create configured functions
const createEmailValidator = curry(function (domain, minLength, email) {
if (email.length < minLength) {
return false;
}
return email.endsWith(`@${domain}`);
});
const isCompanyEmail = createEmailValidator("company.com")(5);
console.log(isCompanyEmail("[email protected]")); // true
console.log(isCompanyEmail("[email protected]")); // false (too short)
console.log(isCompanyEmail("[email protected]")); // false (domain mismatch)Partial Application Pattern
Fix part of a function's parameters:
function partial(fn, ...fixedArgs) {
return function (...remainingArgs) {
return fn.apply(this, [...fixedArgs, ...remainingArgs]);
};
}
// Usage examples
function createNotification(type, title, message, options) {
return {
type,
title,
message,
timestamp: new Date(),
...options,
};
}
// Create specific type notification functions
const createError = partial(createNotification, "error");
const createWarning = partial(createNotification, "warning");
const createSuccess = partial(createNotification, "success");
// Create more specific functions
const createErrorWithTitle = partial(createError, "Error Occurred");
// Use
const notification1 = createErrorWithTitle("Failed to save data", {
dismissible: true,
});
const notification2 = createSuccess(
"Operation Complete",
"Data saved successfully",
{ autoClose: 3000 }
);
console.log(notification1);
// { type: 'error', title: 'Error Occurred', message: 'Failed to save data', ... }
console.log(notification2);
// { type: 'success', title: 'Operation Complete', ... }Decorator Pattern
Using closures to wrap and enhance existing functions:
// Performance measurement decorator
function measurePerformance(fn, label) {
return function (...args) {
const start = performance.now();
const result = fn.apply(this, args);
const end = performance.now();
console.log(`${label || fn.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Cache decorator
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Returning cached result");
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Retry decorator
function retry(fn, maxAttempts = 3, delay = 1000) {
return async function (...args) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn.apply(this, args);
} catch (error) {
lastError = error;
console.log(`Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw new Error(
`Failed after ${maxAttempts} attempts: ${lastError.message}`
);
};
}
// Log decorator
function logCalls(fn) {
return function (...args) {
console.log(`Calling ${fn.name} with args:`, args);
const result = fn.apply(this, args);
console.log(`${fn.name} returned:`, result);
return result;
};
}
// Use decorators
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Combine multiple decorators
const optimizedFib = memoize(measurePerformance(fibonacci, "Fibonacci"));
console.log(optimizedFib(40)); // First time, calculate and record time
console.log(optimizedFib(40)); // Second time, return from cacheObserver Pattern
Using closures to implement publish-subscribe pattern:
function createEventEmitter() {
// Private state
const events = new Map();
const onceListeners = new WeakMap();
return {
on(event, callback) {
if (!events.has(event)) {
events.set(event, []);
}
events.get(event).push(callback);
// Return unsubscribe function
return () => this.off(event, callback);
},
once(event, callback) {
const wrapped = (...args) => {
callback.apply(this, args);
this.off(event, wrapped);
};
onceListeners.set(wrapped, callback);
this.on(event, wrapped);
return () => this.off(event, wrapped);
},
off(event, callback) {
if (!events.has(event)) return;
const callbacks = events.get(event);
const index = callbacks.findIndex((cb) => {
return cb === callback || onceListeners.get(cb) === callback;
});
if (index !== -1) {
callbacks.splice(index, 1);
}
},
emit(event, ...args) {
if (!events.has(event)) return;
const callbacks = [...events.get(event)];
callbacks.forEach((callback) => {
callback.apply(this, args);
});
},
removeAllListeners(event) {
if (event) {
events.delete(event);
} else {
events.clear();
}
},
listenerCount(event) {
return events.has(event) ? events.get(event).length : 0;
},
};
}
// Use
const emitter = createEventEmitter();
// Subscribe to events
const unsubscribe = emitter.on("user:login", (user) => {
console.log(`User logged in: ${user.name}`);
});
emitter.once("app:ready", () => {
console.log("App is ready!");
});
// Trigger events
emitter.emit("user:login", { name: "Sarah" });
emitter.emit("user:login", { name: "John" });
emitter.emit("app:ready"); // Triggers only once
emitter.emit("app:ready"); // Won't trigger
// Unsubscribe
unsubscribe();
emitter.emit("user:login", { name: "Michael" }); // Won't triggerState Machine Pattern
Using closures to manage state transitions:
function createStateMachine(initialState, transitions) {
let currentState = initialState;
const listeners = new Map();
function canTransition(toState) {
const allowedTransitions = transitions[currentState];
return allowedTransitions && allowedTransitions.includes(toState);
}
function notifyListeners(from, to) {
const callbacks = listeners.get("transition") || [];
callbacks.forEach((cb) => cb({ from, to, timestamp: new Date() }));
}
return {
getState() {
return currentState;
},
transition(toState) {
if (!canTransition(toState)) {
throw new Error(
`Invalid transition from "${currentState}" to "${toState}"`
);
}
const from = currentState;
currentState = toState;
notifyListeners(from, to);
return currentState;
},
canTransitionTo(toState) {
return canTransition(toState);
},
onTransition(callback) {
if (!listeners.has("transition")) {
listeners.set("transition", []);
}
listeners.get("transition").push(callback);
return () => {
const callbacks = listeners.get("transition");
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
};
},
getAvailableTransitions() {
return transitions[currentState] || [];
},
};
}
// Use state machine
const orderStateMachine = createStateMachine("pending", {
pending: ["processing", "cancelled"],
processing: ["shipped", "cancelled"],
shipped: ["delivered", "returned"],
delivered: ["returned"],
cancelled: [],
returned: [],
});
// Listen for state changes
orderStateMachine.onTransition((transition) => {
console.log(`Order ${transition.from} -> ${transition.to}`);
});
// Execute state transitions
console.log(orderStateMachine.getState()); // "pending"
orderStateMachine.transition("processing");
orderStateMachine.transition("shipped");
orderStateMachine.transition("delivered");
// Check available transitions
console.log(orderStateMachine.getAvailableTransitions()); // ['returned']
// Try invalid transition
try {
orderStateMachine.transition("processing"); // Error!
} catch (error) {
console.error(error.message);
}Command Pattern
Encapsulate operations as objects, supporting undo/redo:
function createCommandManager() {
const history = [];
let currentIndex = -1;
return {
execute(command) {
// Execute command
command.execute();
// Clear history after current position
history.splice(currentIndex + 1);
// Add to history
history.push(command);
currentIndex++;
},
undo() {
if (currentIndex >= 0) {
const command = history[currentIndex];
command.undo();
currentIndex--;
return true;
}
return false;
},
redo() {
if (currentIndex < history.length - 1) {
currentIndex++;
const command = history[currentIndex];
command.execute();
return true;
}
return false;
},
canUndo() {
return currentIndex >= 0;
},
canRedo() {
return currentIndex < history.length - 1;
},
getHistory() {
return history.map((cmd, index) => ({
type: cmd.type,
isCurrent: index === currentIndex,
}));
},
};
}
// Create command factory
function createCommand(type, execute, undo) {
return { type, execute, undo };
}
// Usage example: text editor
const editor = {
content: "",
insert(text) {
this.content += text;
},
delete(length) {
this.content = this.content.slice(0, -length);
},
getContent() {
return this.content;
},
};
const commandManager = createCommandManager();
// Create insert command
function createInsertCommand(text) {
return createCommand(
"insert",
() => editor.insert(text),
() => editor.delete(text.length)
);
}
// Create delete command
function createDeleteCommand(length) {
let deletedText = "";
return createCommand(
"delete",
() => {
deletedText = editor.content.slice(-length);
editor.delete(length);
},
() => editor.insert(deletedText)
);
}
// Use commands
commandManager.execute(createInsertCommand("Hello"));
console.log(editor.getContent()); // "Hello"
commandManager.execute(createInsertCommand(" World"));
console.log(editor.getContent()); // "Hello World"
commandManager.execute(createDeleteCommand(6));
console.log(editor.getContent()); // "Hello"
// Undo
commandManager.undo();
console.log(editor.getContent()); // "Hello World"
// Redo
commandManager.redo();
console.log(editor.getContent()); // "Hello"
console.log(commandManager.getHistory());
// [
// { type: 'insert', isCurrent: false },
// { type: 'insert', isCurrent: false },
// { type: 'delete', isCurrent: true }
// ]Summary
Closure application patterns showcase JavaScript's power and flexibility. Through these patterns, we can:
- Module Pattern: Create private scope, implement data encapsulation
- Singleton Pattern: Ensure only one instance exists
- Factory Pattern: Standardized way to create objects
- Method Chaining: Provide fluent API interfaces
- Currying: Implement function partial application and reuse
- Decorators: Enhance functionality without modifying original functions
- Observer: Implement event-driven architecture
- State Machine: Manage complex state transitions
- Command Pattern: Encapsulate operations, support undo/redo
These patterns are not just theoretical knowledge but best practices proven in real projects. Mastering them can make your code more elegant, maintainable, and extensible. In the next article, we'll explore memory leak issues that closures might cause, and how to effectively avoid and solve these problems.