JavaScript Closures Tutorial - Section 2: Practical Patterns
A factory function is a function that returns a new object. Unlike constructors, you don't use the new keyword. Factory functions leverage closures to create objects with private state.
// Factory function
function createDog(name, breed) {
return {
name: name,
breed: breed,
bark: function() {
console.log(`${name} says woof!`);
}
};
}
const dog1 = createDog("Max", "Golden Retriever");
const dog2 = createDog("Bella", "Poodle");
dog1.bark(); // "Max says woof!"
dog2.bark(); // "Bella says woof!"
Let's compare the three main ways to create objects:
// 1. Constructor function
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
Dog.prototype.bark = function() {
console.log(`${this.name} says woof!`);
};
const dog1 = new Dog("Max", "Golden Retriever");
// 2. ES6 Class
class DogClass {
constructor(name, breed) {
this.name = name;
this.breed = breed;
}
bark() {
console.log(`${this.name} says woof!`);
}
}
const dog2 = new DogClass("Bella", "Poodle");
// 3. Factory function
function createDog(name, breed) {
return {
name,
breed,
bark() {
console.log(`${name} says woof!`);
}
};
}
const dog3 = createDog("Charlie", "Beagle");
new keyword: Simpler to use, no this confusionFactory functions excel at creating private state:
function createBankAccount(accountHolder, initialBalance) {
// Private variables
let balance = initialBalance;
const transactions = [];
// Private helper function
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
date: new Date(),
balance: balance
});
}
// Public interface
return {
getAccountHolder() {
return accountHolder;
},
getBalance() {
return balance;
},
deposit(amount) {
if (amount <= 0) {
return "Amount must be positive";
}
balance += amount;
recordTransaction('deposit', amount);
return `Deposited $${amount}. New balance: $${balance}`;
},
withdraw(amount) {
if (amount <= 0) {
return "Amount must be positive";
}
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
recordTransaction('withdrawal', amount);
return `Withdrew $${amount}. New balance: $${balance}`;
},
getTransactionHistory() {
// Return a copy to prevent modification
return [...transactions];
}
};
}
const account = createBankAccount("Alice", 1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.getTransactionHistory());
// Can't access private variables
console.log(account.balance); // undefined
console.log(account.transactions); // undefined
Factory functions make it easy to compose behaviors:
// Behavior factories
function canEat(state) {
return {
eat(food) {
console.log(`${state.name} is eating ${food}`);
state.energy += 10;
}
};
}
function canSleep(state) {
return {
sleep(hours) {
console.log(`${state.name} slept for ${hours} hours`);
state.energy += hours * 5;
}
};
}
function canPlay(state) {
return {
play(activity) {
if (state.energy < 10) {
console.log(`${state.name} is too tired to play`);
return;
}
console.log(`${state.name} is playing ${activity}`);
state.energy -= 10;
}
};
}
// Compose a dog
function createDog(name) {
const state = {
name,
energy: 50
};
return Object.assign(
{},
{ getEnergy: () => state.energy },
canEat(state),
canSleep(state),
canPlay(state)
);
}
const dog = createDog("Max");
dog.play("fetch"); // "Max is playing fetch"
dog.eat("treats"); // "Max is eating treats"
dog.sleep(8); // "Max slept for 8 hours"
console.log(dog.getEnergy()); // 90
function createTodoList(name) {
const todos = [];
let nextId = 1;
return {
getName() {
return name;
},
addTodo(text) {
const todo = {
id: nextId++,
text,
completed: false,
createdAt: new Date()
};
todos.push(todo);
return todo.id;
},
completeTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = true;
return true;
}
return false;
},
removeTodo(id) {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos.splice(index, 1);
return true;
}
return false;
},
getTodos() {
return todos.map(t => ({...t})); // Return copies
},
getStats() {
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
pending: todos.filter(t => !t.completed).length
};
}
};
}
const myList = createTodoList("Work Tasks");
myList.addTodo("Write documentation");
myList.addTodo("Review pull requests");
myList.addTodo("Deploy to production");
myList.completeTodo(1);
console.log(myList.getStats());
// { total: 3, completed: 1, pending: 2 }
function createEventEmitter() {
const events = {};
return {
on(event, callback) {
if (!events[event]) {
events[event] = [];
}
events[event].push(callback);
// Return unsubscribe function
return () => {
const index = events[event].indexOf(callback);
if (index !== -1) {
events[event].splice(index, 1);
}
};
},
emit(event, data) {
if (!events[event]) return;
events[event].forEach(callback => callback(data));
},
once(event, callback) {
const unsubscribe = this.on(event, (data) => {
callback(data);
unsubscribe();
});
return unsubscribe;
},
removeAllListeners(event) {
if (event) {
delete events[event];
} else {
Object.keys(events).forEach(key => delete events[key]);
}
}
};
}
const emitter = createEventEmitter();
const unsubscribe = emitter.on('data', (data) => {
console.log('Received:', data);
});
emitter.emit('data', { value: 42 }); // "Received: { value: 42 }"
unsubscribe();
emitter.emit('data', { value: 100 }); // No output
this binding issuesnew)newthis binding issuesNow that you understand factory functions, in the next lesson we'll explore the Module Pattern - a powerful way to organize code and create namespaces using closures.