JavaScript Closures Tutorial - Section 2: Practical Patterns
The module pattern uses closures to create encapsulated modules with private and public members. It's one of the most popular design patterns in JavaScript for organizing code.
const myModule = (function() {
// Private variables and functions
let privateVar = "I'm private";
function privateFunction() {
console.log(privateVar);
}
// Public API
return {
publicMethod: function() {
privateFunction();
},
publicVar: "I'm public"
};
})();
myModule.publicMethod(); // "I'm private"
console.log(myModule.publicVar); // "I'm public"
console.log(myModule.privateVar); // undefined
The module pattern uses an IIFE to create a closure immediately:
// IIFE syntax
(function() {
// Code here runs immediately
console.log("I run right away!");
})();
// With return value
const result = (function() {
return "Hello!";
})();
console.log(result); // "Hello!"
const counterModule = (function() {
// Private state
let count = 0;
// Private helper
function logCount() {
console.log(`Current count: ${count}`);
}
// Public API
return {
increment: function() {
count++;
logCount();
},
decrement: function() {
count--;
logCount();
},
reset: function() {
count = 0;
logCount();
},
getCount: function() {
return count;
}
};
})();
counterModule.increment(); // "Current count: 1"
counterModule.increment(); // "Current count: 2"
counterModule.decrement(); // "Current count: 1"
console.log(counterModule.getCount()); // 1
const apiClient = (function() {
// Private configuration
const config = {
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
// Private helper functions
function buildURL(endpoint) {
return `${config.baseURL}${endpoint}`;
}
function handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
function handleError(error) {
console.error('API Error:', error);
throw error;
}
// Public API
return {
get: async function(endpoint) {
try {
const response = await fetch(buildURL(endpoint), {
method: 'GET',
headers: config.headers,
timeout: config.timeout
});
return await handleResponse(response);
} catch (error) {
return handleError(error);
}
},
post: async function(endpoint, data) {
try {
const response = await fetch(buildURL(endpoint), {
method: 'POST',
headers: config.headers,
body: JSON.stringify(data),
timeout: config.timeout
});
return await handleResponse(response);
} catch (error) {
return handleError(error);
}
},
setBaseURL: function(url) {
config.baseURL = url;
},
setTimeout: function(ms) {
config.timeout = ms;
}
};
})();
// Usage
apiClient.get('/users/1').then(user => console.log(user));
A variation where all functions are defined privately and only references are exposed:
const calculatorModule = (function() {
// Private variables
let result = 0;
// Private functions
function add(x) {
result += x;
return this;
}
function subtract(x) {
result -= x;
return this;
}
function multiply(x) {
result *= x;
return this;
}
function divide(x) {
if (x !== 0) {
result /= x;
}
return this;
}
function getResult() {
return result;
}
function reset() {
result = 0;
return this;
}
// Reveal public pointers to private functions
return {
add: add,
subtract: subtract,
multiply: multiply,
divide: divide,
getResult: getResult,
reset: reset
};
})();
calculatorModule.add(10).multiply(2).subtract(5);
console.log(calculatorModule.getResult()); // 15
const loggerModule = (function() {
// Private state
const config = {
level: 'info',
prefix: '[LOG]',
timestamp: true
};
const levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
// Private helper
function formatMessage(level, message) {
let formatted = config.prefix;
if (config.timestamp) {
formatted += ` [${new Date().toISOString()}]`;
}
formatted += ` [${level.toUpperCase()}] ${message}`;
return formatted;
}
function shouldLog(level) {
return levels[level] >= levels[config.level];
}
// Public API
return {
debug: function(message) {
if (shouldLog('debug')) {
console.log(formatMessage('debug', message));
}
},
info: function(message) {
if (shouldLog('info')) {
console.log(formatMessage('info', message));
}
},
warn: function(message) {
if (shouldLog('warn')) {
console.warn(formatMessage('warn', message));
}
},
error: function(message) {
if (shouldLog('error')) {
console.error(formatMessage('error', message));
}
},
setLevel: function(level) {
if (levels.hasOwnProperty(level)) {
config.level = level;
}
},
setPrefix: function(prefix) {
config.prefix = prefix;
},
enableTimestamp: function(enable) {
config.timestamp = enable;
}
};
})();
loggerModule.info('Application started');
loggerModule.setLevel('warn');
loggerModule.info('This won\'t show');
loggerModule.warn('This will show');
The module pattern creates a singleton by default - only one instance exists:
const appState = (function() {
// Private state - shared across entire application
const state = {
user: null,
theme: 'light',
language: 'en'
};
const listeners = [];
function notifyListeners() {
listeners.forEach(listener => listener(state));
}
return {
getState: function() {
return {...state}; // Return copy
},
setUser: function(user) {
state.user = user;
notifyListeners();
},
setTheme: function(theme) {
state.theme = theme;
notifyListeners();
},
setLanguage: function(lang) {
state.language = lang;
notifyListeners();
},
subscribe: function(callback) {
listeners.push(callback);
return () => {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
};
})();
// Subscribe to state changes
const unsubscribe = appState.subscribe((state) => {
console.log('State changed:', state);
});
appState.setTheme('dark'); // Triggers listener
appState.setUser({ name: 'Alice' }); // Triggers listener
// Pass dependencies to module
const userModule = (function(api, logger) {
// Private
let currentUser = null;
async function fetchUser(id) {
try {
const user = await api.get(`/users/${id}`);
logger.info(`Fetched user: ${user.name}`);
return user;
} catch (error) {
logger.error(`Failed to fetch user: ${error.message}`);
throw error;
}
}
// Public
return {
login: async function(id) {
currentUser = await fetchUser(id);
return currentUser;
},
logout: function() {
logger.info(`User ${currentUser.name} logged out`);
currentUser = null;
},
getCurrentUser: function() {
return currentUser;
}
};
})(apiClient, loggerModule);
// Usage
userModule.login(123).then(user => {
console.log('Logged in:', user);
});
Now that you understand the module pattern, in the next lesson we'll explore currying and partial application - powerful functional programming techniques enabled by closures.