Skip to main content
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 new keyword work
  • ES6 class syntax and what “syntactic sugar” means
  • Private fields (#) and how they differ from closures
  • Static methods, getters, and setters
  • Inheritance with extends and super
  • 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?

ProblemWhy It’s Bad
RepetitionSame code copied over and over
Error-proneEasy to make typos or forget properties
Hard to maintainChange one thing? Change it everywhere
No consistencyNothing enforces that all players have the same structure
Memory wasteEach 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 1300
Why 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

Factory functions with closures provide real privacy. Variables inside the factory can’t be accessed or modified from outside, not even through hacks or reflection.
Factory-created objects are plain objects. They don’t have a special prototype chain, so instanceof won’t work. If you need to check object types, use classes instead.
Factories can return different types of objects, partially constructed objects, or even primitives. Classes always return instances of that class.
Factory functions fit well with functional programming patterns. They’re just functions that return data.

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);  // true

The new Keyword — What It Actually Does

When you call new Player("Alice"), JavaScript performs 4 steps:
1

Create a new empty object

JavaScript creates a fresh object: const obj = {}
2

Link the prototype

Sets obj.[[Prototype]] to Constructor.prototype, establishing the prototype chain
3

Execute the constructor

Runs the constructor with this bound to the new object
4

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); // true

The 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 undefined
Always 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);  // true

Classes 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);  // true

Class 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);           // undefined
Common 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

ES2020 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:
FeaturePrivate Fields (#)Closures (Factory)
Syntaxthis.#fieldlet variable inside function
Access errorSyntaxErrorReturns undefined
MemoryEfficient (prototype methods)Each instance has own methods
instanceofWorksDoesn’t work
InheritancePrivate per classNot inherited
Debugger visibilityVisible but inaccessibleVisible 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);         // 100
Pro 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 methods

Mistake 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();  // 'this' is undefined or global!
console.log(counter.count);  // Still 1 - didn't work!

// ✓ 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());  // 1
The 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 button
This is an alternative to manually binding methods with .bind(this) in the constructor.

Classic Interview Questions

Answer:
AspectFactory FunctionES6 Class
SyntaxRegular function returning objectclass keyword
new keywordNot requiredRequired
instanceofDoesn’t workWorks
PrivacyClosures (truly private)Private fields # (truly private)
MemoryEach instance has own methodsMethods shared via prototype
this bindingCan avoid this entirelyMust use this
// Factory - just a function
function createUser(name) {
  return { name, greet() { return `Hi, ${name}!` } }
}

// Class - a blueprint
class User {
  constructor(name) { this.name = name }
  greet() { return `Hi, ${this.name}!` }
}

const u1 = createUser("Alice")  // No 'new'
const u2 = new User("Bob")      // Requires 'new'
Best answer: Explain both syntax differences AND when to use each.
Answer:new performs 4 steps:
  1. Creates a new empty object {}
  2. Links its prototype to Constructor.prototype
  3. Executes the constructor with this bound to the new object
  4. Returns the object (unless constructor returns a different object)
// This is essentially what 'new' does:
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype)
  const result = Constructor.apply(obj, args)
  return (typeof result === 'object' && result !== null) ? result : obj
}
Best answer: Mention all 4 steps and show the simulation code.
Answer:Two ways to achieve true privacy:1. Private Fields (#) in Classes:
class BankAccount {
  #balance = 0
  deposit(amt) { this.#balance += amt }
  getBalance() { return this.#balance }
}
// account.#balance → SyntaxError!
2. Closures in Factory Functions:
function createBankAccount() {
  let balance = 0
  return {
    deposit(amt) { balance += amt },
    getBalance() { return balance }
  }
}
// account.balance → undefined
Not truly private: The _underscore convention is just a naming hint. Those properties are fully accessible.Best answer: Distinguish between the _underscore convention (not private) and the two truly private approaches.
Answer:Use composition when:
  • You need to mix behaviors from multiple sources (a flying fish, a swimming bird)
  • The “is-a” relationship doesn’t make sense
  • You want loose coupling between components
  • You need flexibility to change behaviors at runtime
Use inheritance when:
  • There’s a clear “is-a” hierarchy (Dog is an Animal)
  • You need instanceof checks
  • You want to share implementation, not just interface
// Inheritance problem: What about a penguin that can't fly?
class Bird { fly() {} }
class Penguin extends Bird { fly() { throw Error("Can't fly!") } }  // Awkward!

// Composition solution: Mix behaviors
const canSwim = (state) => ({ swim() { /*...*/ } })
const canWalk = (state) => ({ walk() { /*...*/ } })

function createPenguin(name) {
  const state = { name }
  return { ...canSwim(state), ...canWalk(state) }  // No fly!
}
Best answer: Give the “Gorilla-Banana” problem example and show composition code.

Common Misconceptions

Reality: JavaScript classes are syntactic sugar over prototypes. Under the hood, they still use prototype-based inheritance, not classical inheritance.
class Player {
  constructor(name) { this.name = name }
  attack() { return `${this.name} attacks!` }
}

// Classes ARE functions!
console.log(typeof Player)  // "function"

// Methods are on the prototype, not the instance
console.log(Player.prototype.attack)  // [Function: attack]
This is why JavaScript has quirks like this binding issues that don’t exist in true class-based languages.
Reality: Factory functions can do everything classes can, plus more:
  • True privacy via closures (before # existed)
  • No this binding issues when using closures
  • Return different types based on input
  • No new keyword to forget
// Factory can return different types!
function createShape(type) {
  if (type === 'circle') return { radius: 10, area() { /*...*/ } }
  if (type === 'square') return { side: 10, area() { /*...*/ } }
}

// Classes always return instances of that class
The trade-off is memory efficiency (classes share methods via prototype).
Reality: They’re completely different:
Aspect_underscore#privateField
AccessibilityFully publicTruly private
Convention only?YesNo, enforced
Error on accessNo errorSyntaxError
class Account {
  _balance = 100   // Accessible! Just a convention
  #pin = 1234      // Truly private
}

const acc = new Account()
console.log(acc._balance)  // 100 — works!
// console.log(acc.#pin)   // SyntaxError!
Reality: Classes were added in ES6 (2015), but that doesn’t mean they’re always better. The JavaScript community has moved toward functions in many cases:
  • React: Moved from class components to function components with hooks
  • Functional programming: Favors factory functions and composition
  • Simplicity: Factory functions have fewer footguns (this, new)
Use classes when: You need instanceof, clear hierarchies, or OOP familiarity.Use factories when: You need composition, true privacy, or functional style.

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);  // false

The super Keyword

super does two things:
  1. In constructor: Calls the parent’s constructor (super(...))
  2. 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 classes

Factory 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 function

Inheritance 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!                 │
└─────────────────────────────────────────────────────────────────────┘
AspectInheritanceComposition
Relationship”is-a” (Dog is an Animal)“has-a” (Duck has flying ability)
FlexibilityRigid hierarchyMix and match behaviors
ReuseThrough parent chainThrough behavior functions
CouplingTight (child depends on parent)Loose (behaviors are independent)
TestingHarder (need parent context)Easier (test behaviors in isolation)
Best forClear hierarchies, instanceof neededFlexible combinations, multiple behaviors

Factory vs Class — Which Should You Use?

Side-by-Side Comparison

FeatureFactory FunctionES6 Class
SyntaxRegular functionclass keyword
new keywordNot neededRequired
instanceofDoesn’t workWorks
True privacyClosuresPrivate fields (#)
Memory efficiencyEach instance has own methodsMethods shared via prototype
this bindingCan avoid this with closuresMust be careful with this
InheritanceComposition (flexible)extends (hierarchical)
FamiliarityFunctional styleOOP 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:
  1. Factory functions are regular functions that return objects — simple and flexible
  2. Constructor functions are used with new to create instances — the traditional approach
  3. ES6 classes are syntactic sugar over constructors — cleaner syntax, same behavior
  4. The new keyword creates an object, links its prototype, runs the constructor, and returns the result
  5. Prototype methods are shared by all instances — saves memory
  6. Private fields (#) provide true privacy in classes — can’t be accessed from outside
  7. Closures provide true privacy in factories — variables trapped in function scope
  8. Static methods belong to the class itself, not instances — use for utilities and factory methods
  9. Inheritance (extends) creates “is-a” relationships — use for clear hierarchies
  10. Composition creates “has-a” relationships — more flexible than inheritance
  11. Use classes when you need instanceof, clear hierarchies, or team familiarity
  12. Use factories when you need composition, true privacy, or functional style

Test Your Knowledge

Answer:When you call new Constructor(args):
  1. Create a new empty object ({})
  2. Link the object’s prototype to Constructor.prototype
  3. Execute the constructor with this bound to the new object
  4. Return the object (unless constructor returns a different object)
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);  // Steps 1-2
  const result = Constructor.apply(obj, args);       // Step 3
  return (typeof result === 'object' && result !== null) ? result : obj;  // Step 4
}
Answer:Instance methods are defined in the constructor — each instance gets its own copy:
function Player(name) {
  this.attack = function() { };  // Each player has own attack function
}
Prototype methods are shared by all instances — more memory efficient:
Player.prototype.attack = function() { };  // All players share one function
In ES6 classes, methods defined in the class body are automatically prototype methods:
class Player {
  attack() { }  // This goes on Player.prototype
}
Answer:
AspectPrivate Fields (#)Closures
Syntaxthis.#fieldlet variable in factory
Error on accessSyntaxErrorReturns undefined
MemoryEfficient (shared methods)Each instance has own methods
instanceofWorksDoesn’t work
// Private Fields
class Wallet {
  #balance = 0;
  getBalance() { return this.#balance; }
}
// w.#balance throws SyntaxError

// Closures
function createWallet() {
  let balance = 0;
  return { getBalance() { return balance; } };
}
// w.balance returns undefined
Answer:super() calls the parent class’s constructor. You must call it in a derived class constructor before using this.
class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    // this.breed = breed;  // ERROR! Can't use 'this' yet
    super(name);            // Call parent constructor first
    this.breed = breed;     // Now 'this' is available
  }
}
super.method() calls a parent’s method from within an overriding method.
Answer:Use composition when:
  • You need to mix behaviors from multiple sources (a flying fish, a swimming bird)
  • The “is-a” relationship doesn’t make sense
  • You want loose coupling between components
  • You need flexibility to change behaviors at runtime
Use inheritance when:
  • There’s a clear “is-a” hierarchy (Dog is an Animal)
  • You need instanceof checks
  • You want to share implementation, not just interface
Rule of thumb: “Favor composition over inheritance” — composition is more flexible.
Answer:Classes are called “syntactic sugar” because they don’t add new functionality — they just provide a cleaner syntax for constructor functions and prototypes.
// This class...
class Player {
  constructor(name) { this.name = name; }
  attack() { return `${this.name} attacks!`; }
}

// ...is equivalent to:
function Player(name) { this.name = name; }
Player.prototype.attack = function() { return `${this.name} attacks!`; };

// Both create the same result:
typeof Player === 'function'  // true for both
The class syntax makes the code easier to read and write, but under the hood, JavaScript is still using prototypes.

Frequently Asked Questions

Factory functions are regular functions that return new objects. Classes are syntactic sugar over constructor functions and prototypes, using the class keyword and new operator. Factories offer simpler composition and true privacy through closures, while classes provide a familiar OOP syntax and share methods efficiently via the prototype chain.
No. As stated in the ECMAScript specification, JavaScript classes are primarily syntactic sugar over the existing prototype-based inheritance model. Under the hood, a class declaration creates a constructor function with methods on its prototype. Unlike classical OOP languages, JavaScript does not have true class-based inheritance — it uses prototypal delegation.
Private fields, prefixed with #, are truly private properties that cannot be accessed outside the class body. They were standardized in ECMAScript 2022 and are enforced by the JavaScript engine at the language level. Unlike the underscore convention (_name), private fields throw a SyntaxError if accessed externally.
Use factories when you need true data privacy through closures, want to compose objects from multiple sources, or need to return different object types conditionally. Use classes when you want prototype-based method sharing, need instanceof checks, or work with frameworks that expect class syntax. According to the 2023 State of JS survey, class syntax is widely adopted, but factory patterns remain popular in functional-style codebases.
The new keyword performs four steps: it creates a new empty object, links that object’s prototype to the constructor’s prototype property, executes the constructor with this bound to the new object, and returns the object (unless the constructor explicitly returns a different object). This is the same for both constructor functions and classes.


Reference


Articles


Videos

Last modified on February 17, 2026