Module Patterns and Encapsulation in JavaScript
Module patterns provide code organization and encapsulation. This article covers IIFE, CommonJS, ES6 modules, and advanced encapsulation techniques.
Introduction
Module patterns provide:
- Code organization
- Encapsulation
- Namespace management
- Dependency management
- Code reusability
Understanding modules helps you:
- Organize large codebases
- Prevent global pollution
- Manage dependencies
- Improve maintainability
- Enable code reuse
Immediately Invoked Function Expression (IIFE)
Basic IIFE
// โ
Good: Basic IIFE for encapsulation
(function() {
const privateVar = 'private';
function privateFunction() {
console.log(privateVar);
}
// Public API
window.MyModule = {
publicMethod() {
privateFunction();
}
};
})();
// Usage
MyModule.publicMethod(); // private
console.log(typeof privateVar); // undefined (private)
IIFE with Parameters
// โ
Good: IIFE with dependency injection
(function(window, document, undefined) {
const privateData = {};
function init() {
console.log('Module initialized');
}
window.MyModule = {
init,
getData() {
return privateData;
}
};
})(window, document);
// Usage
MyModule.init(); // Module initialized
Revealing Module Pattern
// โ
Good: Revealing module pattern
const Calculator = (function() {
// Private variables
let result = 0;
// Private functions
function logOperation(operation, value) {
console.log(`${operation}: ${value}`);
}
// Public API
return {
add(value) {
result += value;
logOperation('Add', value);
return this;
},
subtract(value) {
result -= value;
logOperation('Subtract', value);
return this;
},
multiply(value) {
result *= value;
logOperation('Multiply', value);
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
console.log('Reset');
return this;
}
};
})();
// Usage
Calculator.add(5).multiply(2).subtract(3);
console.log(Calculator.getResult()); // 7
CommonJS Modules
Basic CommonJS
// โ
Good: CommonJS module (Node.js)
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
// app.js
const math = require('./math');
console.log(math.add(5, 3)); // 8
CommonJS with Private Members
// โ
Good: CommonJS with encapsulation
// logger.js
const fs = require('fs');
// Private
function formatMessage(level, message) {
return `[${level}] ${new Date().toISOString()}: ${message}`;
}
// Public
module.exports = {
info(message) {
const formatted = formatMessage('INFO', message);
console.log(formatted);
},
error(message) {
const formatted = formatMessage('ERROR', message);
console.error(formatted);
}
};
// app.js
const logger = require('./logger');
logger.info('Application started');
logger.error('An error occurred');
ES6 Modules
Basic ES6 Modules
// โ
Good: ES6 module
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// app.js
import { add, subtract, PI } from './math.js';
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
Default Exports
// โ
Good: Default export
// logger.js
class Logger {
info(message) {
console.log(`[INFO] ${message}`);
}
error(message) {
console.error(`[ERROR] ${message}`);
}
}
export default Logger;
// app.js
import Logger from './logger.js';
const logger = new Logger();
logger.info('Hello');
Named and Default Exports
// โ
Good: Mixed exports
// utils.js
export function helper1() { }
export function helper2() { }
export default class Utils {
static doSomething() { }
}
// app.js
import Utils, { helper1, helper2 } from './utils.js';
Utils.doSomething();
helper1();
Module Aliasing
// โ
Good: Import aliasing
// app.js
import { add as addition, subtract as subtraction } from './math.js';
import * as math from './math.js';
console.log(addition(5, 3)); // 8
console.log(math.add(5, 3)); // 8
Advanced Encapsulation
Closure-based Encapsulation
// โ
Good: Closure-based private members
class BankAccount {
constructor(initialBalance) {
let balance = initialBalance; // Private
this.deposit = function(amount) {
balance += amount;
return balance;
};
this.withdraw = function(amount) {
if (amount > balance) {
throw new Error('Insufficient funds');
}
balance -= amount;
return balance;
};
this.getBalance = function() {
return balance;
};
}
}
// Usage
const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.balance); // undefined (private)
WeakMap for Private Data
// โ
Good: WeakMap for private data
const privateData = new WeakMap();
class User {
constructor(name, email) {
privateData.set(this, {
name,
email,
password: 'secret'
});
}
getName() {
return privateData.get(this).name;
}
getEmail() {
return privateData.get(this).email;
}
// Password is truly private
}
// Usage
const user = new User('John', '[email protected]');
console.log(user.getName()); // John
console.log(user.password); // undefined
Symbol for Private Properties
// โ
Good: Symbol for private properties
const _password = Symbol('password');
const _email = Symbol('email');
class User {
constructor(name, password, email) {
this.name = name;
this[_password] = password;
this[_email] = email;
}
verifyPassword(password) {
return this[_password] === password;
}
getEmail() {
return this[_email];
}
}
// Usage
const user = new User('John', 'secret123', '[email protected]');
console.log(user.name); // John
console.log(user[_password]); // secret123 (accessible but not obvious)
console.log(user.password); // undefined
Private Fields (ES2022)
// โ
Good: Private fields (modern)
class User {
#password; // Private field
#email;
constructor(name, password, email) {
this.name = name;
this.#password = password;
this.#email = email;
}
verifyPassword(password) {
return this.#password === password;
}
getEmail() {
return this.#email;
}
}
// Usage
const user = new User('John', 'secret123', '[email protected]');
console.log(user.name); // John
console.log(user.#password); // SyntaxError (truly private)
Practical Module Patterns
Plugin System
// โ
Good: Plugin system
const PluginManager = (function() {
const plugins = {};
return {
register(name, plugin) {
if (plugins[name]) {
throw new Error(`Plugin ${name} already registered`);
}
plugins[name] = plugin;
console.log(`Plugin ${name} registered`);
},
unregister(name) {
delete plugins[name];
console.log(`Plugin ${name} unregistered`);
},
execute(name, ...args) {
if (!plugins[name]) {
throw new Error(`Plugin ${name} not found`);
}
return plugins[name](...args);
},
list() {
return Object.keys(plugins);
}
};
})();
// Usage
PluginManager.register('uppercase', (str) => str.toUpperCase());
PluginManager.register('lowercase', (str) => str.toLowerCase());
console.log(PluginManager.execute('uppercase', 'hello')); // HELLO
console.log(PluginManager.list()); // ['uppercase', 'lowercase']
Configuration Module
// โ
Good: Configuration module
const Config = (function() {
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
};
return {
get(key) {
return config[key];
},
set(key, value) {
if (!(key in config)) {
throw new Error(`Unknown config key: ${key}`);
}
config[key] = value;
},
getAll() {
return { ...config };
},
reset() {
Object.assign(config, {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: false
});
}
};
})();
// Usage
console.log(Config.get('apiUrl')); // https://api.example.com
Config.set('debug', true);
console.log(Config.getAll()); // { apiUrl: '...', timeout: 5000, debug: true }
Event Bus Module
// โ
Good: Event bus module
const EventBus = (function() {
const events = {};
return {
on(event, callback) {
if (!events[event]) {
events[event] = [];
}
events[event].push(callback);
},
off(event, callback) {
if (events[event]) {
events[event] = events[event].filter(cb => cb !== callback);
}
},
emit(event, data) {
if (events[event]) {
events[event].forEach(callback => callback(data));
}
},
once(event, callback) {
const onceWrapper = (data) => {
callback(data);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
};
})();
// Usage
EventBus.on('user:login', (user) => {
console.log(`${user} logged in`);
});
EventBus.emit('user:login', 'John');
// John logged in
Best Practices
-
Use ES6 modules when possible:
// โ Good export function helper() { } import { helper } from './module.js'; // โ Bad window.helper = function() { } -
Encapsulate private data:
// โ Good class User { #password; constructor(password) { this.#password = password; } } // โ Bad class User { constructor(password) { this.password = password; } } -
Use clear module boundaries:
// โ Good // Each file is a module with clear responsibility // โ Bad // Everything in one file
Common Mistakes
-
Global namespace pollution:
// โ Bad window.myVar = 'value'; window.myFunction = function() { }; // โ Good const MyModule = (function() { return { myFunction() { } }; })(); -
Circular dependencies:
// โ Bad // module-a.js imports module-b // module-b.js imports module-a // โ Good // Restructure to avoid circular dependencies -
Exposing private data:
// โ Bad class User { constructor(password) { this.password = password; // Exposed } } // โ Good class User { #password; constructor(password) { this.#password = password; // Private } }
Summary
Module patterns organize code effectively. Key takeaways:
- IIFE: Encapsulation with functions
- CommonJS: Node.js modules
- ES6 Modules: Modern standard
- Closures: Private data
- WeakMap: Private object data
- Symbols: Private properties
- Private fields: True privacy
- Improves organization
- Enables encapsulation
- Facilitates maintenance
Related Resources
Next Steps
- Learn about SOLID Principles
- Explore Architectural Patterns
- Study Dependency Injection
- Practice module patterns
- Build modular applications
Comments