JavaScriptadvanced

JavaScript Prototypes & Inheritance

Deep dive into JavaScript's prototype chain, constructor functions, Object.create, ES6 classes, and inheritance patterns. Understand how objects delegate behavior in JS.

16 min readยทPublished Mar 3, 2026
prototypesinheritanceoopjavascript

Objects All the Way Down

JavaScript is fundamentally an object-based language. Unlike class-based languages (Java, C#), JavaScript uses prototypal inheritance โ€” objects inherit directly from other objects. There are no classes at the engine level (ES6 class is syntactic sugar). Understanding prototypes is understanding how JavaScript really works.

Every object in JavaScript has a hidden internal link to another object called its prototype. When you access a property on an object and it's not found, JavaScript follows this link to the prototype, then the prototype's prototype, and so on. This is the prototype chain.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ myObj              โ”‚
โ”‚   name: 'Alice'    โ”‚
โ”‚   [[Prototype]] โ”€โ”€โ”€โ”ผโ”€โ”€โ–บ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚ personProto         โ”‚
                          โ”‚   greet: function   โ”‚
                          โ”‚   [[Prototype]] โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ–บ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚ Object.prototype โ”‚
                                                     โ”‚   toString()     โ”‚
                                                     โ”‚   hasOwnProperty โ”‚
                                                     โ”‚   [[Prototype]] โ”€โ”ผโ”€โ”€โ–บ null
                                                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When you call myObj.greet(), JS looks:

  1. On myObj โ€” not found
  2. On personProto (myObj's prototype) โ€” found, execute it
  3. If not there either, continues to Object.prototype, then null (end)

proto vs prototype vs [[Prototype]]

Three related but different concepts that cause massive confusion. Let's clarify:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Term                   โ”‚ What It Is                                    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ [[Prototype]]          โ”‚ The actual internal link (spec-level, hidden) โ”‚
โ”‚ __proto__              โ”‚ Accessor property to read/write [[Prototype]] โ”‚
โ”‚ Function.prototype     โ”‚ Object assigned as [[Prototype]] of instances โ”‚
โ”‚                        โ”‚ created with `new Function()`                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function () {
  return `${this.name} says woof!`;
};

const rex = new Dog('Rex');

// These all refer to the same object:
rex.__proto__ === Dog.prototype;                    // true
Object.getPrototypeOf(rex) === Dog.prototype;       // true

// Dog.prototype is NOT the prototype OF Dog
// It's the prototype FOR instances created with new Dog()
Dog.__proto__ === Function.prototype;               // true (Dog is a function)
Dog (function)
โ”œโ”€โ”€ .prototype โ”€โ”€โ–บ Dog.prototype { bark: fn, constructor: Dog }
โ”‚                       โ”‚
โ”‚                       โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Object.prototype โ”€โ”€โ–บ null
โ”‚
โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Function.prototype โ”€โ”€โ–บ Object.prototype โ”€โ”€โ–บ null

rex (instance)
โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Dog.prototype { bark: fn }

Best practice: Use Object.getPrototypeOf(obj) instead of obj.__proto__. The latter is deprecated (though still supported everywhere).

Constructor Functions

Before ES6 classes, constructor functions were the standard way to create objects with shared behavior.

function Person(name, age) {
  // 'this' refers to the new object being created
  this.name = name;
  this.age = age;
}

// Methods go on the prototype โ€” shared by all instances
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name} and I'm ${this.age}`;
};

Person.prototype.birthday = function () {
  this.age++;
  return this.age;
};

const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);

alice.greet();    // "Hi, I'm Alice and I'm 30"
bob.greet();      // "Hi, I'm Bob and I'm 25"

// Both share the same function reference
alice.greet === bob.greet; // true โ€” same function on prototype

What new Does

When you call new Person('Alice', 30), four things happen:

// new Person('Alice', 30) is roughly equivalent to:

// 1. Create a new empty object
const obj = {};

// 2. Set its [[Prototype]] to Person.prototype
Object.setPrototypeOf(obj, Person.prototype);

// 3. Call Person with 'this' bound to the new object
const result = Person.call(obj, 'Alice', 30);

// 4. Return the object (unless Person returned an object)
return result instanceof Object ? result : obj;

Why Methods Go on the Prototype

If methods were inside the constructor, every instance would get its own copy:

// BAD โ€” each instance gets a separate copy of greet
function Person(name) {
  this.name = name;
  this.greet = function () {
    return `Hi, I'm ${this.name}`;
  };
}

const a = new Person('A');
const b = new Person('B');
a.greet === b.greet; // false โ€” two different function objects

// GOOD โ€” single copy shared via prototype
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

const c = new Person('C');
const d = new Person('D');
c.greet === d.greet; // true โ€” same function object

For 1000 instances, the bad pattern creates 1000 function objects. The prototype pattern creates 1.

Object.create

Object.create creates a new object with a specified prototype. It's the most direct way to set up prototypal inheritance.

const animal = {
  type: 'Animal',
  describe() {
    return `${this.name} is a ${this.type}`;
  },
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.type = 'Dog';
dog.bark = function () {
  return `${this.name} says woof!`;
};

dog.describe(); // "Rex is a Dog" โ€” found on animal prototype
dog.bark();     // "Rex says woof!" โ€” found on dog itself

Object.create(null) โ€” No Prototype

Creates a truly empty object with no prototype chain:

const dict = Object.create(null);

dict.toString;       // undefined โ€” no Object.prototype
dict.hasOwnProperty; // undefined

// Useful for pure dictionaries/maps
dict['key'] = 'value';

// No prototype pollution risk
'toString' in dict; // false

Multi-Level Prototype Chain

const living = {
  isAlive: true,
  breathe() {
    return 'breathing...';
  },
};

const animal = Object.create(living);
animal.eat = function () {
  return 'eating...';
};

const dog = Object.create(animal);
dog.bark = function () {
  return 'woof!';
};

const rex = Object.create(dog);
rex.name = 'Rex';

rex.bark();     // 'woof!'     โ€” from dog
rex.eat();      // 'eating...' โ€” from animal
rex.breathe();  // 'breathing...' โ€” from living
rex.isAlive;    // true        โ€” from living
rex.name;       // 'Rex'       โ€” own property
rex
  โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ dog { bark }
        โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ animal { eat }
              โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ living { isAlive, breathe }
                    โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Object.prototype
                          โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ null

ES6 Classes

ES6 class syntax is syntactic sugar over prototype-based inheritance. Under the hood, it does the same thing as constructor functions + prototypes.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hi, I'm ${this.name}`;
  }

  birthday() {
    this.age++;
    return this.age;
  }
}

const alice = new Person('Alice', 30);
alice.greet(); // "Hi, I'm Alice"

// Under the hood:
typeof Person;                          // 'function'
alice.__proto__ === Person.prototype;    // true
Person.prototype.greet === alice.greet;  // true

Static Methods and Properties

class MathUtils {
  static PI = 3.14159265;

  static add(a, b) {
    return a + b;
  }

  static random(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}

MathUtils.add(2, 3);    // 5
MathUtils.PI;            // 3.14159265

// Static methods exist on the class itself, NOT on instances
const m = new MathUtils();
m.add; // undefined

Private Fields and Methods (ES2022)

True private members using the # prefix:

class BankAccount {
  #balance;
  #pin;

  constructor(initialBalance, pin) {
    this.#balance = initialBalance;
    this.#pin = pin;
  }

  #validatePin(pin) {
    return pin === this.#pin;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('Invalid amount');
    this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount, pin) {
    if (!this.#validatePin(pin)) throw new Error('Invalid PIN');
    if (amount > this.#balance) throw new Error('Insufficient funds');
    this.#balance -= amount;
    return this.#balance;
  }

  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000, '1234');
account.balance;              // 1000 (getter)
account.deposit(500);         // 1500
account.withdraw(200, '1234'); // 1300

// account.#balance;          // SyntaxError: Private field
// account.#validatePin;      // SyntaxError: Private field

Getters and Setters

class Temperature {
  #celsius;

  constructor(celsius) {
    this.#celsius = celsius;
  }

  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }

  set fahrenheit(f) {
    this.#celsius = (f - 32) * 5 / 9;
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(c) {
    if (c < -273.15) throw new Error('Below absolute zero');
    this.#celsius = c;
  }
}

const temp = new Temperature(100);
temp.fahrenheit; // 212
temp.fahrenheit = 32;
temp.celsius;    // 0

Inheritance Patterns

Constructor Function Inheritance (Pre-ES6)

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

function Dog(name, breed) {
  Animal.call(this, name); // call parent constructor
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // fix constructor reference

Dog.prototype.bark = function () {
  return `${this.name} barks!`;
};

const rex = new Dog('Rex', 'Shepherd');
rex.speak(); // "Rex makes a sound" โ€” inherited from Animal
rex.bark();  // "Rex barks!" โ€” own method

rex instanceof Dog;    // true
rex instanceof Animal; // true
rex
  โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Dog.prototype { bark, constructor: Dog }
        โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Animal.prototype { speak, constructor: Animal }
              โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ Object.prototype
                    โ””โ”€โ”€ [[Prototype]] โ”€โ”€โ–บ null

ES6 Class Inheritance

Much cleaner syntax with extends and super:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // MUST call super() before using 'this'
    this.breed = breed;
  }

  bark() {
    return `${this.name} barks!`;
  }

  // Override parent method
  speak() {
    return `${this.name} barks loudly!`;
  }
}

class GuideDog extends Dog {
  constructor(name, breed, owner) {
    super(name, breed);
    this.owner = owner;
  }

  assist() {
    return `${this.name} guides ${this.owner}`;
  }

  // Call parent's original speak
  speak() {
    return `${super.speak()} while guiding ${this.owner}`;
  }
}

const buddy = new GuideDog('Buddy', 'Labrador', 'Alice');
buddy.speak();   // "Buddy barks loudly! while guiding Alice"
buddy.bark();    // "Buddy barks!"
buddy.assist();  // "Buddy guides Alice"

Mixin Pattern โ€” Multiple Inheritance Workaround

JavaScript doesn't support multiple inheritance, but you can compose behavior with mixins:

const Serializable = (Base) =>
  class extends Base {
    serialize() {
      return JSON.stringify(this);
    }

    static deserialize(json) {
      return Object.assign(new this(), JSON.parse(json));
    }
  };

const Validatable = (Base) =>
  class extends Base {
    validate() {
      for (const [key, rules] of Object.entries(this.constructor.rules || {})) {
        for (const rule of rules) {
          if (!rule.test(this[key])) {
            throw new Error(`${key}: ${rule.message}`);
          }
        }
      }
      return true;
    }
  };

const Timestamped = (Base) =>
  class extends Base {
    constructor(...args) {
      super(...args);
      this.createdAt = Date.now();
      this.updatedAt = Date.now();
    }

    touch() {
      this.updatedAt = Date.now();
    }
  };

// Compose mixins
class User extends Serializable(Validatable(Timestamped(class {}))) {
  static rules = {
    name: [{ test: (v) => v?.length > 0, message: 'Name required' }],
    email: [{ test: (v) => v?.includes('@'), message: 'Invalid email' }],
  };

  constructor(name, email) {
    super();
    this.name = name;
    this.email = email;
  }
}

const user = new User('Alice', '[email protected]');
user.validate();    // true
user.serialize();   // '{"createdAt":...,"updatedAt":...,"name":"Alice","email":"[email protected]"}'
user.createdAt;     // timestamp

Composition Over Inheritance

Modern JS tends to prefer composition. Instead of deep inheritance chains, compose behavior:

// Instead of: class FlyingSwimmingAnimal extends SwimmingAnimal extends Animal
// Use composition:

function withSwimming(obj) {
  return {
    ...obj,
    swim() {
      return `${obj.name} is swimming`;
    },
  };
}

function withFlying(obj) {
  return {
    ...obj,
    fly() {
      return `${obj.name} is flying`;
    },
  };
}

function withSpeaking(obj) {
  return {
    ...obj,
    speak(sound) {
      return `${obj.name} says ${sound}`;
    },
  };
}

function createDuck(name) {
  const base = { name };
  return withSpeaking(withSwimming(withFlying(base)));
}

const donald = createDuck('Donald');
donald.swim();          // "Donald is swimming"
donald.fly();           // "Donald is flying"
donald.speak('quack');  // "Donald says quack"

Type Checking โ€” instanceof, typeof, and More

typeof

Returns a string indicating the type. Limited for objects.

typeof 42;            // 'number'
typeof 'hello';       // 'string'
typeof true;          // 'boolean'
typeof undefined;     // 'undefined'
typeof null;          // 'object'  โ† famous bug, never fixed
typeof {};            // 'object'
typeof [];            // 'object'  โ† arrays are objects
typeof function(){};  // 'function'
typeof Symbol();      // 'symbol'
typeof 10n;           // 'bigint'

instanceof

Checks if an object's prototype chain includes a constructor's prototype:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const rex = new Dog();

rex instanceof Dog;    // true
rex instanceof Animal; // true โ€” Dog extends Animal
rex instanceof Cat;    // false
rex instanceof Object; // true โ€” everything extends Object

// How instanceof works internally:
function myInstanceOf(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

Object.keys, Object.values, Object.entries

These only return own enumerable properties (not inherited ones):

const parent = { inherited: true };
const child = Object.create(parent);
child.own = 'yes';
child.also = 'mine';

Object.keys(child);     // ['own', 'also'] โ€” no 'inherited'
Object.values(child);   // ['yes', 'mine']
Object.entries(child);  // [['own', 'yes'], ['also', 'mine']]

// To check for inherited properties:
'inherited' in child;            // true โ€” checks whole chain
child.hasOwnProperty('inherited'); // false โ€” own only
child.hasOwnProperty('own');       // true

Checking the Full Prototype Chain

function getPrototypeChain(obj) {
  const chain = [];
  let current = Object.getPrototypeOf(obj);

  while (current !== null) {
    chain.push(current);
    current = Object.getPrototypeOf(current);
  }

  return chain;
}

class A {}
class B extends A {}
class C extends B {}

const c = new C();
getPrototypeChain(c);
// [C.prototype, B.prototype, A.prototype, Object.prototype]

Property Lookup and Shadowing

When you access a property, JS walks the prototype chain. When you set a property, it always goes on the object itself (creating a "shadow"):

const parent = {
  x: 10,
  greet() {
    return 'hello from parent';
  },
};

const child = Object.create(parent);

// Reading โ€” walks the chain
child.x;       // 10 (found on parent)
child.greet(); // 'hello from parent' (found on parent)

// Writing โ€” creates own property (shadow)
child.x = 20;

child.x;       // 20 (own property, shadows parent)
parent.x;      // 10 (unchanged)

child.hasOwnProperty('x'); // true โ€” now has its own
Before child.x = 20:                After child.x = 20:

child { }                           child { x: 20 }   โ† shadow
  โ””โ”€โ”€ parent { x: 10 }               โ””โ”€โ”€ parent { x: 10 }  โ† still 10

child.x โ†’ 10 (from parent)         child.x โ†’ 20 (own property)

Gotcha: Shadowing with Methods

const base = {
  count: 0,
  increment() {
    this.count++; // 'this' is the calling object, not base
  },
};

const obj = Object.create(base);

obj.increment();
obj.count; // 1 โ€” created as own property on obj
base.count; // 0 โ€” base is untouched

// this.count++ is equivalent to:
// this.count = this.count + 1;
// Reading this.count โ†’ finds 0 on base (prototype chain)
// Writing this.count โ†’ creates own property on obj

Performance Considerations

Prototype Chain Length

Longer chains mean more lookups. Keep inheritance shallow:

// Deep chain (avoid) โ€” each property lookup traverses 5 levels
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
class E extends D {}

// Shallow (preferred) โ€” max 2 levels
class Base {}
class Widget extends Base {}

hasOwnProperty in Hot Paths

If you're iterating frequently, checking hasOwnProperty adds overhead. Use Object.keys() or Object.entries() which only return own properties:

const obj = Object.create({ inherited: true });
obj.a = 1;
obj.b = 2;

// Slow in hot loop โ€” checks hasOwnProperty each iteration
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    process(obj[key]);
  }
}

// Faster โ€” Object.keys returns only own enumerable properties
for (const key of Object.keys(obj)) {
  process(obj[key]);
}

Modifying Built-in Prototypes (Don't)

You can add methods to Array.prototype, String.prototype, etc. You absolutely should not:

// DO NOT DO THIS
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 โ€” works, but...

// Problems:
// 1. Conflicts with future language features (Array.prototype.at)
// 2. Breaks for...in loops on arrays
// 3. Conflicts with other libraries doing the same thing
// 4. Makes debugging nightmarish

Object.freeze and Prototypes

Object.freeze only freezes own properties. The prototype is still mutable:

const proto = { shared: 'mutable' };
const obj = Object.create(proto);
obj.own = 'frozen';

Object.freeze(obj);

obj.own = 'changed';     // silently fails (or throws in strict mode)
obj.own;                 // 'frozen' โ€” frozen

proto.shared = 'changed';
obj.shared;              // 'changed' โ€” prototype wasn't frozen

Common Patterns Summary

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Pattern              โ”‚ Pros                   โ”‚ Cons                    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Constructor          โ”‚ Familiar, instanceof   โ”‚ Verbose, manual proto   โ”‚
โ”‚ Functions            โ”‚ works                  โ”‚ chain setup             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ ES6 Classes          โ”‚ Clean syntax, extends, โ”‚ Sugar over prototypes,  โ”‚
โ”‚                      โ”‚ super, static, private โ”‚ can mislead about how   โ”‚
โ”‚                      โ”‚                        โ”‚ JS actually works       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Object.create        โ”‚ Direct prototype link, โ”‚ No constructor,         โ”‚
โ”‚                      โ”‚ simple, explicit       โ”‚ verbose for many props  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Factory Functions    โ”‚ No 'new', no 'this',   โ”‚ No instanceof,          โ”‚
โ”‚ (closures)           โ”‚ true privacy           โ”‚ memory per instance     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Mixins               โ”‚ Compose multiple       โ”‚ Can be complex,         โ”‚
โ”‚                      โ”‚ behaviors              โ”‚ name collisions         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Composition          โ”‚ Flexible, no deep      โ”‚ No instanceof,          โ”‚
โ”‚                      โ”‚ hierarchies            โ”‚ more verbose            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Practical Example: Plugin System

A real-world example combining prototypes with extensibility:

class EventEmitter {
  #listeners = new Map();

  on(event, callback) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, []);
    }
    this.#listeners.get(event).push(callback);
    return this;
  }

  emit(event, ...args) {
    const callbacks = this.#listeners.get(event) || [];
    for (const cb of callbacks) {
      cb.apply(this, args);
    }
    return this;
  }

  off(event, callback) {
    const callbacks = this.#listeners.get(event);
    if (callbacks) {
      this.#listeners.set(
        event,
        callbacks.filter((cb) => cb !== callback)
      );
    }
    return this;
  }
}

class PluginSystem extends EventEmitter {
  #plugins = new Map();

  register(name, plugin) {
    if (this.#plugins.has(name)) {
      throw new Error(`Plugin "${name}" already registered`);
    }

    // Validate plugin has required interface
    if (typeof plugin.init !== 'function') {
      throw new Error(`Plugin "${name}" must have init() method`);
    }

    this.#plugins.set(name, plugin);
    plugin.init(this);
    this.emit('plugin:registered', { name, plugin });
    return this;
  }

  getPlugin(name) {
    return this.#plugins.get(name);
  }
}

// Create a plugin
const loggingPlugin = {
  init(system) {
    system.on('plugin:registered', ({ name }) => {
      console.log(`[Logger] Plugin registered: ${name}`);
    });
  },
  log(message) {
    console.log(`[${new Date().toISOString()}] ${message}`);
  },
};

const app = new PluginSystem();
app.register('logger', loggingPlugin);
app.getPlugin('logger').log('System started');

Key Takeaways

  1. Every object has a [[Prototype]] โ€” the engine follows it to find inherited properties
  2. prototype is a property on functions โ€” used as the [[Prototype]] for instances created with new
  3. Use Object.getPrototypeOf() โ€” not __proto__ (deprecated)
  4. ES6 classes are sugar โ€” they compile to constructor functions + prototypes
  5. Properties shadow, not override โ€” setting a property on a child creates an own property, parent unchanged
  6. Keep chains shallow โ€” deep inheritance hierarchies hurt readability and performance
  7. Prefer composition over inheritance โ€” compose small behaviors instead of deep class trees
  8. Don't modify built-in prototypes โ€” it causes conflicts and hard-to-debug issues
  9. Object.create(null) for dictionaries โ€” no prototype chain means no inherited property surprises
  10. Understanding prototypes is non-negotiable โ€” even if you use classes, the prototype system is what runs underneath

Found this helpful?

Support devsofus โ€” help us keep creating free dev guides.

Related Articles