Factory Functions vs Constructor Functions vs Classes

TL;DR:
- Factory functions are plain functions that return new objects—flexible and don’t require
new
. - Constructor functions use
new
to build objects with shared prototypes—classic ES5 pattern. - ES6 classes are syntactic sugar over constructor/prototype patterns—clearer syntax, built-in inheritance.
Use factory functions when you want privacy and flexibility, constructor functions in legacy/ES5 codebases, and ES6 classes for clear inheritance and encapsulation.
1. Factory Functions
Factory functions are simple functions that create and return new objects. They let you encapsulate private data via closures, and you don’t have to worry about using new
.
function createUser(name, age) { // Private helper let secret = Math.random() return { name, age, getSecret() { return secret }, } } const alice = createUser('Alice', 30) console.log(alice.name) // → "Alice" console.log(alice.getSecret()) // → some random number
- No
new
keyword required. - Private state via closed-over variables.
- Flexibility: return different shapes of objects based on parameters.
2. Constructor Functions
Constructor functions are regular functions intended to be called with new
. Under the hood, they create a fresh object, bind this
, and return it (unless another object is explicitly returned).
function User(name, age) { this.name = name this.age = age } User.prototype.greet = function () { console.log(`Hi, I'm ${this.name}`) } const bob = new User('Bob', 25) bob.greet() // → "Hi, I'm Bob"
- Use
new User(...)
to instantiate. - Shared methods via
User.prototype
. - Error-prone if you forget
new
:this
will beundefined
(in strict mode) or global.
3. ES6 Classes
Classes in ES6 provide a clearer, more concise syntax for constructor functions and inheritance, but they’re just syntactic sugar over prototypes.
class User { #secret // private field constructor(name, age) { this.name = name this.age = age this.#secret = Math.random() } getSecret() { return this.#secret } greet() { console.log(`Hello, ${this.name}`) } } const carol = new User('Carol', 28) carol.greet() // → "Hello, Carol" console.log(carol.getSecret()) // → some random number
class
keyword withconstructor
and methods.- Private fields via
#
. extends
for inheritance:class Admin extends User { … }
.
4. Visualizing the Differences
Factory: createUser() ──► returns new object literal (closure)
Constructor: new User() ──► Object ↔ User.prototype
Class: new User() ──► Object ↔ User.prototype (ES6 syntax)
- Factory returns distinct objects each call.
- Constructors & classes share methods on prototype.
- Classes bundle constructor + prototype methods in one declaration.
5. Pitfalls & Best Practices
Pattern | Pitfall | Tip |
---|---|---|
Factory | All instances carry own method copies | Extract common behavior into shared helpers |
Constructor | Forgetting new leads to bugs | Use linters or new.target checks |
Classes | Overusing inheritance can create tight coupling | Favor composition over deep class hierarchies |
6. When to Use Which
-
Factory Functions:
- You need private state or variable return shapes.
- You prefer composition over inheritance.
-
Constructor Functions:
- Working in ES5 environments or incremental codebases.
- You want prototype-based sharing without ES6 syntax.
-
ES6 Classes:
- You need clear inheritance chains.
- You want built-in support for private fields and static methods.
By understanding these three patterns, you’ll choose the right tool for creating objects—balancing simplicity, performance, and maintainability in your JavaScript code.