How do you create hundreds of similar objects without copy-pasting? How do game developers spawn thousands of enemies? How does JavaScript let you build blueprints for objects?
// Factory function — returns a new object each time
function createPlayer(name) {
return {
name,
health: 100,
attack() {
return `${this.name} attacks!`
}
}
}
// Class — a blueprint for creating objects
class Enemy {
constructor(name) {
this.name = name
this.health = 100
}
attack() {
return `${this.name} attacks!`
}
}
// Both create objects the same way
const player = createPlayer("Alice") // Factory
const enemy = new Enemy("Goblin") // Class
console.log(player.attack()) // "Alice attacks!"
console.log(enemy.attack()) // "Goblin attacks!"Factories and Classes are two patterns for creating objects efficiently. A factory function is a regular function that returns a new object. A class is a blueprint that uses the class keyword and the new operator. Both achieve the same goal, but they work differently and have different strengths. According to the 2023 State of JS survey, class syntax is now widely adopted, with the majority of JavaScript developers using classes regularly in their projects.
What you'll learn in this guide:
- How to create objects using factory functions
- How constructor functions and the
newkeyword work - ES6 class syntax and what "syntactic sugar" means
- Private fields (#) and how they differ from closures
- Static methods, getters, and setters
- Inheritance with
extendsandsuper - Factory composition vs class inheritance
- When to use factories vs classes
Prerequisites: This guide assumes you understand Object Creation & Prototypes and this, call, apply, bind. If those concepts are new to you, read those guides first!
Why Do We Need Object Blueprints?
The Manual Approach (Don't Do This)
Let's say you're building an RPG game. You need player characters:
// Creating players manually — tedious and error-prone
const player1 = {
name: "Alice",
health: 100,
level: 1,
attack() {
return `${this.name} attacks for ${10 + this.level * 2} damage!`;
},
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
return `${this.name} has been defeated!`;
}
return `${this.name} has ${this.health} health remaining.`;
}
};
const player2 = {
name: "Bob",
health: 100,
level: 1,
attack() {
return `${this.name} attacks for ${10 + this.level * 2} damage!`;
},
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
return `${this.name} has been defeated!`;
}
return `${this.name} has ${this.health} health remaining.`;
}
};
// ... 50 more players with the same code copied ...What's Wrong With This?
| Problem | Why It's Bad |
|---|---|
| Repetition | Same code copied over and over |
| Error-prone | Easy to make typos or forget properties |
| Hard to maintain | Change one thing? Change it everywhere |
| No consistency | Nothing enforces that all players have the same structure |
| Memory waste | Each object has its own copy of the methods |
What We Need
We need a way to:
- Define the structure once
- Create as many objects as we need
- Ensure all objects have the same properties and methods
- Make changes in one place that affect all objects
The Assembly Line Analogy
Think about how real-world manufacturing works:
- Hand-crafting each item individually is slow, inconsistent, and doesn't scale
- Assembly lines (factories) take specifications and produce products efficiently
- Blueprints/molds define the template once, then stamp out identical copies
JavaScript gives us the same options:
┌─────────────────────────────────────────────────────────────────────────┐
│ THREE WAYS TO CREATE OBJECTS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ MANUAL CREATION Like hand-carving each chess piece │
│ ─────────────── Tedious, error-prone, inconsistent │
│ const obj = { ... } │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ FACTORY FUNCTION Like an assembly line │
│ ──────────────── Put in specs → Get product │
│ Flexible, no special keywords │
│ createPlayer("Alice") │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Player │ ← New object returned │
│ │ {name...} │ │
│ └─────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ CLASS / CONSTRUCTOR Like a blueprint or mold │
│ ─────────────────── Define template → Stamp out copies │
│ Uses `new`, supports `instanceof` │
│ new Player("Alice") │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Player │ ← Instance created from blueprint │
│ │ {name...} │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘Both factories and classes solve the same problem. They just do it differently. Let's explore each approach.
What is a Factory Function in JavaScript?
A factory function is a regular JavaScript function that creates and returns a new object each time it's called. Unlike constructors or classes, factory functions don't require the new keyword. They can use this in returned methods (like simple objects do), or use closures to avoid this entirely, giving you flexibility that classes don't offer. As Douglas Crockford documented in JavaScript: The Good Parts, factory functions leverage JavaScript's prototypal nature more directly than class-based patterns.
Basic Factory Function
Think of it like an assembly line. You put in the specifications, and it produces the product:
// A simple factory function
function createPlayer(name) {
return {
name: name,
health: 100,
level: 1,
attack() {
return `${this.name} attacks for ${10 + this.level * 2} damage!`;
},
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
return `${this.name} has been defeated!`;
}
return `${this.name} has ${this.health} health remaining.`;
}
};
}
// Creating players is now easy!
const alice = createPlayer("Alice");
const bob = createPlayer("Bob");
const charlie = createPlayer("Charlie");
console.log(alice.attack()); // "Alice attacks for 12 damage!"
console.log(bob.takeDamage(30)); // "Bob has 70 health remaining."Factory with Multiple Parameters
function createEnemy(name, health, attackPower) {
return {
name, // Shorthand: same as name: name
health,
attackPower,
isAlive: true,
attack(target) {
return `${this.name} attacks ${target.name} for ${this.attackPower} damage!`;
},
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
this.health = 0;
this.isAlive = false;
return `${this.name} has been defeated!`;
}
return `${this.name} has ${this.health} health remaining.`;
}
};
}
// Create different types of enemies
const goblin = createEnemy("Goblin", 50, 10);
const dragon = createEnemy("Dragon", 500, 50);
const boss = createEnemy("Dark Lord", 1000, 100);
console.log(goblin.attack(dragon)); // "Goblin attacks Dragon for 10 damage!"
console.log(dragon.takeDamage(100)); // "Dragon has 400 health remaining."Factory with Configuration Object
For many options, use a configuration object:
function createCharacter(config) {
// Default values
const defaults = {
name: "Unknown",
health: 100,
maxHealth: 100,
level: 1,
experience: 0,
attackPower: 10,
defense: 5
};
// Merge defaults with provided config
const settings = { ...defaults, ...config };
return {
...settings,
attack(target) {
const damage = Math.max(0, this.attackPower - target.defense);
return `${this.name} deals ${damage} damage to ${target.name}!`;
},
heal(amount) {
this.health = Math.min(this.maxHealth, this.health + amount);
return `${this.name} healed to ${this.health} health.`;
},
gainExperience(amount) {
this.experience += amount;
if (this.experience >= this.level * 100) {
this.level++;
this.experience = 0;
this.attackPower += 5;
return `${this.name} leveled up to ${this.level}!`;
}
return `${this.name} gained ${amount} XP.`;
}
};
}
// Create characters with different configurations
const warrior = createCharacter({
name: "Warrior",
health: 150,
maxHealth: 150,
attackPower: 20,
defense: 10
});
const mage = createCharacter({
name: "Mage",
health: 80,
maxHealth: 80,
attackPower: 30,
defense: 3
});
// Only override what you need
const villager = createCharacter({ name: "Villager" });Factory with Private Variables (Closures)
A powerful feature of factory functions is creating truly private variables using closures:
function createBankAccount(ownerName, initialBalance = 0) {
// Private variables — NOT accessible from outside
let balance = initialBalance;
const transactionHistory = [];
// Private function
function recordTransaction(type, amount) {
transactionHistory.push({
type,
amount,
balance,
date: new Date().toISOString()
});
}
// Initialize
recordTransaction("opening", initialBalance);
// Return public interface
return {
owner: ownerName,
deposit(amount) {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
balance += amount;
recordTransaction("deposit", amount);
return `Deposited $${amount}. New balance: $${balance}`;
},
withdraw(amount) {
if (amount <= 0) {
throw new Error("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new Error("Insufficient funds");
}
balance -= amount;
recordTransaction("withdrawal", amount);
return `Withdrew $${amount}. New balance: $${balance}`;
},
getBalance() {
return balance;
},
getStatement() {
return transactionHistory.map(t =>
`${t.date}: ${t.type} $${t.amount} (Balance: $${t.balance})`
).join('\n');
}
};
}
const account = createBankAccount("Alice", 1000);
console.log(account.deposit(500)); // "Deposited $500. New balance: $1500"
console.log(account.withdraw(200)); // "Withdrew $200. New balance: $1300"
console.log(account.getBalance()); // 1300
// Trying to access private variables — FAILS!
console.log(account.balance); // undefined
console.log(account.transactionHistory); // undefined
// Can't cheat!
account.balance = 1000000; // Does nothing useful
console.log(account.getBalance()); // Still 1300Why is this private? The variables balance and transactionHistory exist only inside the factory function. The returned object's methods can access them through closure, but nothing outside can. This is true encapsulation!
Factory Creating Different Types
Factories can return different object types based on input:
function createWeapon(type) {
const weapons = {
sword: {
name: "Iron Sword",
damage: 25,
speed: "medium",
attack() {
return `Slash with ${this.name} for ${this.damage} damage!`;
}
},
bow: {
name: "Longbow",
damage: 20,
speed: "fast",
range: 100,
attack() {
return `Fire an arrow for ${this.damage} damage from ${this.range}m away!`;
}
},
staff: {
name: "Magic Staff",
damage: 35,
speed: "slow",
manaCost: 10,
attack() {
return `Cast a spell for ${this.damage} damage! (Costs ${this.manaCost} mana)`;
}
}
};
if (!weapons[type]) {
throw new Error(`Unknown weapon type: ${type}`);
}
return { ...weapons[type] }; // Return a copy
}
const sword = createWeapon("sword");
const bow = createWeapon("bow");
const staff = createWeapon("staff");
console.log(sword.attack()); // "Slash with Iron Sword for 25 damage!"
console.log(bow.attack()); // "Fire an arrow for 20 damage from 100m away!"
console.log(staff.attack()); // "Cast a spell for 35 damage! (Costs 10 mana)"When to Use Factory Functions
How Do Constructor Functions Work?
A constructor function is a regular JavaScript function designed to be called with the new keyword. When invoked with new, it creates a new object, binds this to that object, and returns it automatically. Constructor names conventionally start with a capital letter to distinguish them from regular functions. This was the standard way to create objects before ES6 classes.
Basic Constructor Function
// Convention: Constructor names start with a capital letter
function Player(name) {
// 'this' refers to the new object being created
this.name = name;
this.health = 100;
this.level = 1;
this.attack = function() {
return `${this.name} attacks for ${10 + this.level * 2} damage!`;
};
}
// Create instances with 'new'
const alice = new Player("Alice");
const bob = new Player("Bob");
console.log(alice.name); // "Alice"
console.log(bob.attack()); // "Bob attacks for 12 damage!"
console.log(alice instanceof Player); // trueThe new Keyword — What It Actually Does
When you call new Player("Alice"), JavaScript performs 4 steps:
Create a new empty object
JavaScript creates a fresh object: const obj = {}
Link the prototype
Sets obj.[[Prototype]] to Constructor.prototype, establishing the prototype chain
Execute the constructor
Runs the constructor with this bound to the new object
Return the object
Returns obj automatically (unless the constructor explicitly returns a different non-null object; primitive return values are ignored)
Want to dive deeper? For a detailed explanation of how new works under the hood, including how to simulate it yourself, see Object Creation & Prototypes.
Adding Methods to the Prototype
There's a problem with our constructor: each instance gets its own copy of methods:
function Player(name) {
this.name = name;
this.health = 100;
// BAD: Every player gets their own copy of this function
this.attack = function() {
return `${this.name} attacks!`;
};
}
const p1 = new Player("Alice");
const p2 = new Player("Bob");
// These are different functions!
console.log(p1.attack === p2.attack); // false
// 1000 players = 1000 copies of attack function = wasted memory!The solution is to put methods on the prototype:
function Player(name) {
this.name = name;
this.health = 100;
// Don't put methods here!
}
// Add methods to the prototype — shared by all instances
Player.prototype.attack = function() {
return `${this.name} attacks!`;
};
Player.prototype.takeDamage = function(amount) {
this.health -= amount;
return `${this.name} has ${this.health} health.`;
};
const p1 = new Player("Alice");
const p2 = new Player("Bob");
// Now they share the same function!
console.log(p1.attack === p2.attack); // true
// 1000 players = 1 copy of attack function = efficient!┌─────────────────────────────────────────────────────────────────────┐
│ PROTOTYPE CHAIN │
│ │
│ Player.prototype │
│ ┌─────────────────────────┐ │
│ │ attack: function() │ │
│ │ takeDamage: function() │◄──── Shared by all instances │
│ └─────────────────────────┘ │
│ ▲ │
│ │ [[Prototype]] │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ p1 │ │ p2 │ │
│ │─────────│ │─────────│ │
│ │name: │ │name: │ │
│ │"Alice" │ │"Bob" │ │
│ │health: │ │health: │ │
│ │100 │ │100 │ │
│ └─────────┘ └─────────┘ │
│ │
│ Each instance has its own data, but shares methods via prototype │
└─────────────────────────────────────────────────────────────────────┘The instanceof Operator
instanceof checks if an object was created by a constructor:
function Player(name) {
this.name = name;
}
function Enemy(name) {
this.name = name;
}
const alice = new Player("Alice");
const goblin = new Enemy("Goblin");
console.log(alice instanceof Player); // true
console.log(alice instanceof Enemy); // false
console.log(goblin instanceof Enemy); // true
console.log(goblin instanceof Player); // false
// Both are instances of Object
console.log(alice instanceof Object); // true
console.log(goblin instanceof Object); // trueThe Problem: Forgetting new
function Player(name) {
this.name = name;
this.health = 100;
}
// Oops! Forgot 'new'
const alice = Player("Alice");
console.log(alice); // undefined (function returned nothing)
console.log(name); // "Alice" — LEAKED to global scope!
console.log(health); // 100 — ALSO leaked!
// In strict mode, this would throw an error instead
// 'use strict';
// Player("Alice"); // TypeError: Cannot set property 'name' of undefinedAlways use new with constructor functions! Without it, this refers to the global object (or undefined in strict mode), causing bugs that are hard to track down.
What Are ES6 Classes in JavaScript?
An ES6 class is JavaScript's modern syntax for creating constructor functions and prototypes. Introduced in ECMAScript 2015, classes provide a cleaner, more familiar syntax for object-oriented programming while working exactly the same as constructor functions under the hood. They're often called "syntactic sugar." Classes use the class keyword and require the new operator to create instances.
Basic Class Syntax
class Player {
constructor(name) {
this.name = name;
this.health = 100;
this.level = 1;
}
attack() {
return `${this.name} attacks for ${10 + this.level * 2} damage!`;
}
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
return `${this.name} has been defeated!`;
}
return `${this.name} has ${this.health} health remaining.`;
}
}
const alice = new Player("Alice");
console.log(alice.attack()); // "Alice attacks for 12 damage!"
console.log(alice instanceof Player); // trueClasses Are "Syntactic Sugar"
Classes don't add new functionality. They're just a nicer way to write constructor functions. Under the hood, they work exactly the same:
class Enemy {
constructor(name, health) {
this.name = name;
this.health = health;
}
attack() {
return `${this.name} attacks!`;
}
static createBoss(name) {
return new Enemy(name, 1000);
}
}Both create objects with the same structure:
// Both versions produce:
const goblin = new Enemy("Goblin", 100);
console.log(typeof Enemy); // "function" (classes ARE functions!)
console.log(goblin.constructor === Enemy); // true
console.log(goblin.__proto__ === Enemy.prototype); // trueClass Syntax Breakdown
class Character {
// Class field (public property with default value)
level = 1;
experience = 0;
// Constructor — called when you use 'new'
constructor(name, health = 100) {
this.name = name;
this.health = health;
}
// Instance method — available on all instances
attack() {
return `${this.name} attacks!`;
}
// Another instance method
heal(amount) {
this.health += amount;
return `${this.name} healed to ${this.health} HP.`;
}
// Getter — accessed like a property
get isAlive() {
return this.health > 0;
}
// Setter — assigned like a property
set healthPoints(value) {
this.health = Math.max(0, value); // Can't go below 0
}
// Static method — called on the class, not instances
static createHero(name) {
return new Character(name, 150);
}
// Static property
static MAX_LEVEL = 99;
}
// Usage
const hero = Character.createHero("Alice"); // Static method
console.log(hero.attack()); // Instance method
console.log(hero.isAlive); // Getter (no parentheses!)
hero.healthPoints = -50; // Setter
console.log(hero.health); // 0 (setter prevented negative)
console.log(Character.MAX_LEVEL); // 99 (static property)Static Methods and Properties
Static members belong to the class itself, not to instances:
class MathUtils {
// Static properties
static PI = 3.14159;
static E = 2.71828;
// Static methods
static square(x) {
return x * x;
}
static cube(x) {
return x * x * x;
}
static randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
// Access via class name
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.square(5)); // 25
// NOT via instances!
const utils = new MathUtils();
console.log(utils.PI); // undefined
console.log(utils.square); // undefinedCommon uses for static methods:
- Factory methods (
User.fromJSON(data)) - Utility functions (
Array.isArray(value)) - Singleton patterns (
Config.getInstance())
Getters and Setters
Getters and setters let you define computed properties and add validation:
class Temperature {
constructor(celsius) {
this._celsius = celsius; // Convention: underscore = "private"
}
// Getter: accessed like a property
get celsius() {
return this._celsius;
}
// Setter: assigned like a property
set celsius(value) {
if (value < -273.15) {
throw new Error("Temperature below absolute zero!");
}
this._celsius = value;
}
// Computed getter: fahrenheit from celsius
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
// Computed setter: set celsius from fahrenheit
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // Uses celsius setter for validation
}
// Read-only getter (no setter)
get kelvin() {
return this._celsius + 273.15;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
temp.fahrenheit = 100; // Set via fahrenheit
console.log(temp.celsius); // ~37.78 (converted)
// temp.celsius = -300; // Error: Temperature below absolute zero!
// temp.kelvin = 0; // Error: no setter (read-only)Private Fields (#) — True Privacy
ES2022 introduced private fields with the # prefix. Unlike the _underscore convention, these are truly private:
class BankAccount {
// Private fields — declared with #
#balance = 0;
#pin;
#transactionHistory = [];
constructor(ownerName, initialBalance, pin) {
this.ownerName = ownerName; // Public
this.#balance = initialBalance;
this.#pin = pin;
}
// Private method
#recordTransaction(type, amount) {
this.#transactionHistory.push({
type,
amount,
balance: this.#balance,
date: new Date()
});
}
// Private method for PIN verification
#verifyPin(pin) {
return this.#pin === pin;
}
// Public methods
deposit(amount) {
if (amount <= 0) throw new Error("Invalid amount");
this.#balance += amount;
this.#recordTransaction("deposit", amount);
return this.#balance;
}
withdraw(amount, pin) {
if (!this.#verifyPin(pin)) {
throw new Error("Invalid PIN");
}
if (amount > this.#balance) {
throw new Error("Insufficient funds");
}
this.#balance -= amount;
this.#recordTransaction("withdrawal", amount);
return this.#balance;
}
getBalance(pin) {
if (!this.#verifyPin(pin)) {
throw new Error("Invalid PIN");
}
return this.#balance;
}
}
const account = new BankAccount("Alice", 1000, "1234");
account.deposit(500);
console.log(account.withdraw(200, "1234")); // 1300
console.log(account.getBalance("1234")); // 1300
// Trying to access private fields — ALL FAIL
// account.#balance; // SyntaxError!
// account.#pin; // SyntaxError!
// account.#verifyPin("1234"); // SyntaxError!
console.log(account.balance); // undefined (different property)Private Fields (#) vs Closure-Based Privacy
Both provide true privacy, but they work differently:
| Feature | Private Fields (#) | Closures (Factory) |
|---|---|---|
| Syntax | this.#field | let variable inside function |
| Access error | SyntaxError | Returns undefined |
| Memory | Efficient (prototype methods) | Each instance has own methods |
instanceof | Works | Doesn't work |
| Inheritance | Private per class | Not inherited |
| Debugger visibility | Visible but inaccessible | Visible in closure scope |
// Private Fields (#)
class Wallet {
#balance = 0;
deposit(amount) { this.#balance += amount; }
getBalance() { return this.#balance; }
}
const w1 = new Wallet();
const w2 = new Wallet();
console.log(w1.deposit === w2.deposit); // true (shared via prototype)
// Closure-based (Factory)
function createWallet() {
let balance = 0;
return {
deposit(amount) { balance += amount; },
getBalance() { return balance; }
};
}
const w3 = createWallet();
const w4 = createWallet();
console.log(w3.deposit === w4.deposit); // false (each has own copy)Common Mistakes with Factories and Classes
When working with factories and classes, there are several common pitfalls that trip up developers. Let's look at the most frequent mistakes and how to avoid them.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE 3 MOST COMMON MISTAKES │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. FORGETTING `new` WITH CONSTRUCTORS │
│ Pollutes global scope or crashes in strict mode │
│ │
│ 2. FORGETTING `super()` IN DERIVED CLASSES │
│ Must call super() before using `this` │
│ │
│ 3. CONFUSING `_private` WITH TRULY PRIVATE │
│ Underscore is just a convention, not enforcement │
│ │
└─────────────────────────────────────────────────────────────────────────┘Mistake 1: Forgetting new with Constructor Functions
// ❌ WRONG - Forgot 'new', 'this' becomes global object
function Player(name) {
this.name = name;
this.health = 100;
}
const alice = Player("Alice"); // Missing 'new'!
console.log(alice); // undefined
console.log(globalThis.name); // "Alice" - leaked to global!
console.log(globalThis.health); // 100 - also leaked!
// ✓ CORRECT - Always use 'new' with constructors
const bob = new Player("Bob");
console.log(bob.name); // "Bob"
console.log(bob.health); // 100Pro tip: Use ES6 classes instead of constructor functions — they throw an error if you forget new:
class Player {
constructor(name) { this.name = name; }
}
const alice = Player("Alice"); // TypeError: Class constructor Player cannot be invoked without 'new'Mistake 2: Forgetting super() in Derived Classes
// ❌ WRONG - Using 'this' before calling super()
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
this.breed = breed; // ReferenceError: Must call super before accessing 'this'
super(name);
}
}
// ✓ CORRECT - Call super() first, then use 'this'
class Cat extends Animal {
constructor(name, color) {
super(name); // Initialize parent first
this.color = color; // Now 'this' is available
}
}
const kitty = new Cat("Whiskers", "orange");
console.log(kitty.name); // "Whiskers"
console.log(kitty.color); // "orange"Mistake 3: Thinking _underscore Means Private
// ❌ WRONG - Underscore is just a naming convention
class BankAccount {
constructor(balance) {
this._balance = balance; // Not actually private!
}
getBalance() {
return this._balance;
}
}
const account = new BankAccount(1000);
console.log(account._balance); // 1000 - fully accessible!
account._balance = 999999; // Can be modified!
console.log(account.getBalance()); // 999999 - no protection!
// ✓ CORRECT - Use private fields (#) for true privacy
class SecureBankAccount {
#balance; // Truly private
constructor(balance) {
this.#balance = balance;
}
getBalance() {
return this.#balance;
}
}
const secure = new SecureBankAccount(1000);
// console.log(secure.#balance); // SyntaxError!
console.log(secure.getBalance()); // 1000 - only accessible via methodsMistake 4: Using this Incorrectly in Factory Functions
// ❌ WRONG - 'this' in factory return object can cause issues
function createCounter() {
return {
count: 0,
increment() {
this.count++; // 'this' depends on how the method is called
}
};
}
const counter = createCounter();
counter.increment(); // Works
console.log(counter.count); // 1
const increment = counter.increment;
increment(); // Strict mode: TypeError ('this' is undefined)
// Sloppy mode: increments globalThis.count (NaN)
console.log(counter.count); // Still 1 (if no error was thrown)
// ✓ CORRECT - Use closure to avoid 'this' issues
function createSafeCounter() {
let count = 0; // Closure variable
return {
increment() {
count++; // No 'this' needed
},
getCount() {
return count;
}
};
}
const safeCounter = createSafeCounter();
const safeIncrement = safeCounter.increment;
safeIncrement(); // Works even when extracted!
console.log(safeCounter.getCount()); // 1The this Trap: When you extract a method from an object and call it standalone, this is no longer bound to the original object. Factory functions that use closures instead of this avoid this problem entirely.
Arrow Function Class Fields: In classes, you can use arrow functions as class fields to auto-bind this:
class Button {
count = 0;
// Arrow function automatically binds 'this' to the instance
handleClick = () => {
this.count++;
console.log(`Clicked ${this.count} times`);
};
}
const button = new Button();
const handler = button.handleClick;
handler(); // Works! 'this' is still bound to buttonThis is an alternative to manually binding methods with .bind(this) in the constructor.
Classic Interview Questions
Common Misconceptions
How Does Inheritance Work in JavaScript?
Class Inheritance with extends
Use extends to create a class that inherits from another:
// Base class (parent)
class Character {
constructor(name, health) {
this.name = name;
this.health = health;
}
attack() {
return `${this.name} attacks!`;
}
takeDamage(amount) {
this.health -= amount;
return `${this.name} has ${this.health} HP left.`;
}
isAlive() {
return this.health > 0;
}
}
// Derived class (child)
class Warrior extends Character {
constructor(name) {
super(name, 150); // Call parent constructor
this.armor = 20; // Add new property
}
// Override parent method
takeDamage(amount) {
const reduced = Math.max(0, amount - this.armor);
return super.takeDamage(reduced); // Call parent method
}
// New method only for Warriors
shieldBash() {
return `${this.name} bashes with shield for ${this.armor} damage!`;
}
}
// Another derived class
class Mage extends Character {
constructor(name) {
super(name, 80); // Mages have less health
this.mana = 100;
}
// Override with different behavior
attack() {
if (this.mana >= 10) {
this.mana -= 10;
return `${this.name} casts fireball for 50 damage! (Mana: ${this.mana})`;
}
return `${this.name} is out of mana! Basic attack for 5 damage.`;
}
meditate() {
this.mana = Math.min(100, this.mana + 30);
return `${this.name} meditates. Mana: ${this.mana}`;
}
}
// Usage
const conan = new Warrior("Conan");
const gandalf = new Mage("Gandalf");
console.log(conan.attack()); // "Conan attacks!"
console.log(conan.takeDamage(30)); // "Conan has 140 HP left." (reduced by armor)
console.log(conan.shieldBash()); // "Conan bashes with shield for 20 damage!"
console.log(gandalf.attack()); // "Gandalf casts fireball for 50 damage! (Mana: 90)"
console.log(gandalf.meditate()); // "Gandalf meditates. Mana: 100"
// instanceof works through the chain
console.log(conan instanceof Warrior); // true
console.log(conan instanceof Character); // true
console.log(gandalf instanceof Mage); // true
console.log(gandalf instanceof Warrior); // falseThe super Keyword
super does two things:
- In constructor: Calls the parent's constructor (
super(...)) - In methods: Accesses parent's methods (
super.method())
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
// MUST call super() before using 'this' in derived class
super(name); // Calls Animal's constructor
this.breed = breed;
}
speak() {
// Call parent method and add to it
const parentSays = super.speak();
return `${parentSays} Specifically: Woof!`;
}
}
const rex = new Dog("Rex", "German Shepherd");
console.log(rex.speak());
// "Rex makes a sound. Specifically: Woof!"In a derived class constructor, you MUST call super() before using this. JavaScript needs to initialize the parent part of the object first.
class Child extends Parent {
constructor(name) {
// this.name = name; // ERROR! Can't use 'this' yet
super(); // Must call super first
this.name = name; // Now 'this' is available
}
}The Problem with Deep Inheritance
Inheritance can become problematic with deep hierarchies:
// The "Gorilla-Banana Problem"
class Animal { }
class Mammal extends Animal { }
class Primate extends Mammal { }
class Ape extends Primate { }
class Gorilla extends Ape { }
// You wanted a banana, but you got the whole jungle!
// - Deep chains are hard to understand
// - Changes to parent classes can break children
// - Tight coupling between classesFactory Composition — A Flexible Alternative
Instead of inheritance ("is-a"), use composition ("has-a"):
// Define behaviors as small, focused functions
const canWalk = (state) => ({
walk() {
state.position += state.speed;
return `${state.name} walks to position ${state.position}`;
}
});
const canSwim = (state) => ({
swim() {
state.position += state.speed * 1.5;
return `${state.name} swims to position ${state.position}`;
}
});
const canFly = (state) => ({
fly() {
state.position += state.speed * 3;
return `${state.name} flies to position ${state.position}`;
}
});
const canSpeak = (state) => ({
speak(message) {
return `${state.name} says: "${message}"`;
}
});
// Compose characters by mixing behaviors
function createDuck(name) {
const state = { name, position: 0, speed: 2 };
return {
name: state.name,
...canWalk(state),
...canSwim(state),
...canFly(state),
...canSpeak(state),
getPosition: () => state.position
};
}
function createPenguin(name) {
const state = { name, position: 0, speed: 1 };
return {
name: state.name,
...canWalk(state),
...canSwim(state),
// No canFly! Penguins can't fly
...canSpeak(state),
getPosition: () => state.position
};
}
function createFish(name) {
const state = { name, position: 0, speed: 4 };
return {
name: state.name,
...canSwim(state),
// Fish can only swim
getPosition: () => state.position
};
}
// Usage
const donald = createDuck("Donald");
donald.walk(); // "Donald walks to position 2"
donald.swim(); // "Donald swims to position 5"
donald.fly(); // "Donald flies to position 11"
donald.speak("Quack!"); // 'Donald says: "Quack!"'
const tux = createPenguin("Tux");
tux.walk(); // Works
tux.swim(); // Works
// tux.fly(); // TypeError: tux.fly is not a function
const nemo = createFish("Nemo");
nemo.swim(); // Works
// nemo.walk(); // TypeError: nemo.walk is not a function
// nemo.fly(); // TypeError: nemo.fly is not a functionInheritance vs Composition
┌─────────────────────────────────────────────────────────────────────┐
│ INHERITANCE (is-a) │
│ │
│ Animal Problem: What about flying fish? │
│ │ What about penguins that can't fly? │
│ ├── Bird (can fly) What about bats (mammals that fly)? │
│ │ └── Penguin ??? │
│ ├── Fish (can swim) You end up with awkward hierarchies │
│ │ └── FlyingFish ??? or lots of override methods. │
│ └── Mammal │
│ └── Bat ??? │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ COMPOSITION (has-a) │
│ │
│ Behaviors: Characters: │
│ ┌─────────┐ ┌───────────────────────────────────────┐ │
│ │ canWalk │─────────│ Duck = canWalk + canSwim + canFly │ │
│ └─────────┘ │ Penguin = canWalk + canSwim │ │
│ ┌─────────┐ │ Fish = canSwim │ │
│ │ canSwim │─────────│ FlyingFish = canSwim + canFly │ │
│ └─────────┘ │ Bat = canWalk + canFly │ │
│ ┌─────────┐ └───────────────────────────────────────┘ │
│ │ canFly │ │
│ └─────────┘ Mix and match any combination! │
└─────────────────────────────────────────────────────────────────────┘| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | "is-a" (Dog is an Animal) | "has-a" (Duck has flying ability) |
| Flexibility | Rigid hierarchy | Mix and match behaviors |
| Reuse | Through parent chain | Through behavior functions |
| Coupling | Tight (child depends on parent) | Loose (behaviors are independent) |
| Testing | Harder (need parent context) | Easier (test behaviors in isolation) |
| Best for | Clear hierarchies, instanceof needed | Flexible combinations, multiple behaviors |
Factory vs Class — Which Should You Use?
Side-by-Side Comparison
| Feature | Factory Function | ES6 Class |
|---|---|---|
| Syntax | Regular function | class keyword |
new keyword | Not needed | Required |
instanceof | Doesn't work | Works |
| True privacy | Closures | Private fields (#) |
| Memory efficiency | Each instance has own methods | Methods shared via prototype |
this binding | Can avoid this with closures | Must be careful with this |
| Inheritance | Composition (flexible) | extends (hierarchical) |
| Familiarity | Functional style | OOP style (familiar to Java/C# devs) |
When to Use Factory Functions
Need true privacy
Closure-based privacy can't be circumvented
No instanceof needed
You don't need to check object types
Composition over inheritance
Mix and match behaviors flexibly
Functional programming style
Fits well with functional patterns
When to Use Classes
Need instanceof
Type checking at runtime
Clear hierarchies
When "is-a" relationships make sense
Team familiarity
Team knows OOP from other languages
Framework requirements
React components, Angular services, etc.
Decision Guide
┌─────────────────────────────────────────────────────────────────────┐
│ WHICH SHOULD I USE? │
│ │
│ Do you need instanceof checks? │
│ YES ──► Use Class │
│ NO ──▼ │
│ │
│ Do you need a clear inheritance hierarchy? │
│ YES ──► Use Class with extends │
│ NO ──▼ │
│ │
│ Do you need to mix multiple behaviors? │
│ YES ──► Use Factory with composition │
│ NO ──▼ │
│ │
│ Do you need truly private data? │
│ YES ──► Either works (Factory closures OR Class with #) │
│ NO ──▼ │
│ │
│ Is your team familiar with OOP? │
│ YES ──► Use Class (more familiar syntax) │
│ NO ──► Use Factory (simpler mental model) │
└─────────────────────────────────────────────────────────────────────┘Real-World Examples
React Components (Classes → Functions)
// Old: Class components
class Button extends React.Component {
render() {
return <button>{this.props.label}</button>;
}
}
// Modern: Function components (like factories)
function Button({ label }) {
return <button>{label}</button>;
}Game Entities (Classes for hierarchy)
class Entity { }
class Character extends Entity { }
class Player extends Character { }
class NPC extends Character { }Utility Objects (Factories for flexibility)
const logger = createLogger({ level: 'debug', prefix: '[App]' });
const cache = createCache({ maxSize: 100, ttl: 3600 });Key Takeaways
The key things to remember:
-
Factory functions are regular functions that return objects — simple and flexible
-
Constructor functions are used with
newto create instances — the traditional approach -
ES6 classes are syntactic sugar over constructors — cleaner syntax, same behavior
-
The
newkeyword creates an object, links its prototype, runs the constructor, and returns the result -
Prototype methods are shared by all instances — saves memory
-
Private fields (#) provide true privacy in classes — can't be accessed from outside
-
Closures provide true privacy in factories — variables trapped in function scope
-
Static methods belong to the class itself, not instances — use for utilities and factory methods
-
Inheritance (
extends) creates "is-a" relationships — use for clear hierarchies -
Composition creates "has-a" relationships — more flexible than inheritance
-
Use classes when you need
instanceof, clear hierarchies, or team familiarity -
Use factories when you need composition, true privacy, or functional style
Test Your Knowledge
Frequently Asked Questions
Related Concepts
Object Creation & Prototypes
Deep dive into JavaScript's prototype chain, Object.create(), and how the new keyword works under the hood
Content coming soon.this, call, apply, bind
Understanding this binding in different contexts
Content coming soon.Inheritance and Polymorphism
Advanced inheritance patterns and polymorphism in JavaScript
Content coming soon.Design Patterns
Common patterns including Factory, Singleton, and more
Content coming soon.Reference
Classes — MDN
Official MDN documentation on ES6 classes
Private class features — MDN
Documentation on private fields and methods
new operator — MDN
How the new keyword works
Object.create() — MDN
Creating objects with specific prototypes
Articles
How To Use Classes in JavaScript
Tania builds a Character class step by step, adding features one at a time. Great if you want to follow along and type the code yourself.
JavaScript Classes — Under The Hood
Shows the ES5 equivalent of every ES6 class feature side by side. Read this to understand what JavaScript is really doing when you write a class.
Factory Functions in JavaScript
A classic introduction to factory functions using a Car example. Shows the self-pattern for avoiding this issues and private variables with closures.
Class vs Factory function
Cristi Salcescu's comparison of both approaches with pros, cons, and when to use each.
Composition vs Inheritance
Uses a game character example to show how composition avoids the problems of deep inheritance. Includes the mixin pattern for adding behaviors.
Understanding super in JavaScript
Explains when and why you need super() with clear error examples. Covers the "must call super before this" rule that trips up beginners.
Videos
JavaScript Factory Functions
Mosh builds a circle factory from scratch in under 10 minutes. Good starting point if you've never seen factories before.
Factory Functions in JavaScript
MPJ's signature conversational style makes factories feel approachable. Includes the "why not just use classes?" discussion.
Composition over Inheritance
Fun Fun Function explains why composition is often better than inheritance with the "Gorilla-Banana" problem.
JavaScript Classes Tutorial
Traversy covers classes from basic syntax to private fields in one video. Watch at 1.5x speed for a quick refresher.