JavaScript Closures Tutorial - Section 2: Practical Patterns
In JavaScript, object properties are public by default. Anyone can access and modify them:
const user = {
name: "Alice",
balance: 1000
};
// Anyone can modify the balance!
user.balance = 999999;
console.log(user.balance); // 999999 - Oops!
Closures allow us to create truly private variables that can only be accessed through controlled methods.
The basic pattern for private variables:
function createUser(name, initialBalance) {
// Private variables
let balance = initialBalance;
const accountNumber = Math.random().toString(36).substr(2, 9);
// Public interface
return {
getName: function() {
return name;
},
getBalance: function() {
return balance;
},
deposit: function(amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
},
getAccountNumber: function() {
// Return masked version
return `***${accountNumber.slice(-4)}`;
}
};
}
const alice = createUser("Alice", 1000);
console.log(alice.getBalance()); // 1000
alice.deposit(500);
console.log(alice.getBalance()); // 1500
// Can't access private variables directly
console.log(alice.balance); // undefined
console.log(alice.accountNumber); // undefined
Use closures to create a configuration object with private settings:
function createConfig() {
// Private configuration
const config = {
apiKey: "secret-key-12345",
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};
// Private validation
function isValidTimeout(value) {
return typeof value === 'number' && value > 0 && value < 30000;
}
// Public interface
return {
getApiUrl: function() {
return config.apiUrl;
},
getTimeout: function() {
return config.timeout;
},
setTimeout: function(value) {
if (isValidTimeout(value)) {
config.timeout = value;
return true;
}
return false;
},
getRetries: function() {
return config.retries;
},
// Never expose the API key directly
hasApiKey: function() {
return config.apiKey !== null;
}
};
}
const appConfig = createConfig();
console.log(appConfig.getTimeout()); // 5000
appConfig.setTimeout(10000);
console.log(appConfig.getTimeout()); // 10000
// Can't access the API key!
console.log(appConfig.apiKey); // undefined
Create a secure password manager with private storage:
function createPasswordManager(masterPassword) {
// Private storage
const passwords = {};
let isUnlocked = false;
// Private helper
function hash(str) {
// Simplified hash (use proper hashing in production!)
return btoa(str);
}
return {
unlock: function(password) {
if (hash(password) === hash(masterPassword)) {
isUnlocked = true;
return true;
}
return false;
},
lock: function() {
isUnlocked = false;
},
addPassword: function(service, password) {
if (!isUnlocked) {
return "Manager is locked";
}
passwords[service] = hash(password);
return "Password saved";
},
getPassword: function(service) {
if (!isUnlocked) {
return "Manager is locked";
}
return passwords[service] ? "Password exists" : "No password found";
},
isLocked: function() {
return !isUnlocked;
}
};
}
const manager = createPasswordManager("master123");
console.log(manager.isLocked()); // true
manager.unlock("master123");
manager.addPassword("email", "mypass123");
console.log(manager.getPassword("email")); // "Password exists"
manager.lock();
console.log(manager.getPassword("email")); // "Manager is locked"
// Can't access passwords directly!
console.log(manager.passwords); // undefined
A popular pattern that defines all functions privately and reveals only what's needed:
const calculator = (function() {
// Private variables and functions
let result = 0;
function validateNumber(n) {
return typeof n === 'number' && !isNaN(n);
}
function add(n) {
if (validateNumber(n)) {
result += n;
}
return this;
}
function subtract(n) {
if (validateNumber(n)) {
result -= n;
}
return this;
}
function multiply(n) {
if (validateNumber(n)) {
result *= n;
}
return this;
}
function divide(n) {
if (validateNumber(n) && n !== 0) {
result /= n;
}
return this;
}
function getResult() {
return result;
}
function reset() {
result = 0;
return this;
}
// Reveal public interface
return {
add: add,
subtract: subtract,
multiply: multiply,
divide: divide,
getResult: getResult,
reset: reset
};
})();
calculator.add(10).multiply(2).subtract(5);
console.log(calculator.getResult()); // 15
// Can't access private functions
console.log(calculator.validateNumber); // undefined
console.log(calculator.result); // undefined
You can also have private methods that are only used internally:
function createLogger(prefix) {
// Private method
function formatMessage(level, message) {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${prefix}] [${level}] ${message}`;
}
// Public methods
return {
info: function(message) {
console.log(formatMessage('INFO', message));
},
warn: function(message) {
console.warn(formatMessage('WARN', message));
},
error: function(message) {
console.error(formatMessage('ERROR', message));
}
};
}
const logger = createLogger('MyApp');
logger.info('Application started');
logger.warn('Low memory');
logger.error('Connection failed');
// Can't call formatMessage directly
// logger.formatMessage('DEBUG', 'test'); // Error!
Now that you understand data privacy with closures, in the next lesson we'll explore factory functions - a powerful pattern for creating multiple objects with private state.