Learning Objectives

  • Understand what factory functions are
  • Compare factory functions to constructors and classes
  • Create objects with private state
  • Implement composition with factory functions

What Are Factory Functions?

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!"

Factory Functions vs Constructors

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");

Advantages of Factory Functions

  • No new keyword: Simpler to use, no this confusion
  • True privacy: Can have truly private variables
  • Flexible: Can return any object structure
  • Composition-friendly: Easy to mix behaviors
  • No prototype chain: Simpler mental model

Private State with Factory Functions

Factory 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

Composition with Factory Functions

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

Practical Example: Todo List

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 }

Practical Example: Event Emitter

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

When to Use Factory Functions

  • When you need true private variables
  • When you want to avoid this binding issues
  • When composition is more important than inheritance
  • When creating utility objects or modules
  • When you want a simpler API (no new)

Key Takeaways

  • ✅ Factory functions return new objects without using new
  • ✅ They provide true privacy through closures
  • ✅ No this binding issues
  • ✅ Perfect for composition over inheritance
  • ✅ Each instance has independent private state

Next Steps

Now 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.