Skip to content
JavaScript js prototypes 4 min read

Prototypal Inheritance

Most languages inherit by copying a blueprint into each instance. JavaScript does something simpler and more powerful: objects delegate to other objects through the prototype chain. Instead of duplicating behavior, an object simply points at another object and borrows its properties at lookup time. Understanding this delegation model lets you reuse code cleanly, and it demystifies what class actually compiles down to.

Inheritance is delegation

When you read a property the engine can’t find on an object, it follows the object’s [[Prototype]] link and looks there instead. So “inheritance” in JavaScript is really live lookup through a chain of objects. Set up that chain and the child object behaves as if it owns the parent’s methods, without ever copying them.

The most direct way to build a chain is Object.create, which makes a new object whose prototype is exactly the object you pass in:

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

const dog = Object.create(animal);
dog.name = "Rex";
dog.type = "dog";

console.log(dog.describe());                          // delegated to animal
console.log(Object.getPrototypeOf(dog) === animal);   // true

Output:

Rex is a dog
true

dog owns only name and type. The describe method lives on animal, but this inside it still refers to dog, so delegation reads the right data.

Multi-level chains

Because a prototype is just an object, it can have its own prototype. Chaining Object.create calls builds a hierarchy where each level adds behavior and the next level down delegates upward.

const animal = {
  eat() {
    return `${this.name} eats`;
  },
};

const dog = Object.create(animal);
dog.bark = function () {
  return `${this.name} barks`;
};

const puppy = Object.create(dog);
puppy.name = "Milo";

console.log(puppy.bark()); // found on dog
console.log(puppy.eat());  // found on animal

Output:

Milo barks
Milo eats

The chain is puppydoganimalObject.prototypenull. Lookup stops at the first match, so closer prototypes naturally override more distant ones.

Constructor inheritance with Object.create + call

Before class, the canonical way to make one constructor inherit from another used two pieces working together: Parent.call(this, ...) to run the parent’s initializer on the new instance, and Object.create(Parent.prototype) to wire the prototype chain for shared methods.

function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function () {
  return `${this.name} eats`;
};

function Dog(name, breed) {
  Animal.call(this, name); // 1. inherit instance properties
  this.breed = breed;
}

// 2. inherit prototype methods
Dog.prototype = Object.create(Animal.prototype);
// 3. restore the constructor reference
Dog.prototype.constructor = Dog;

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

const d = new Dog("Rex", "Lab");
console.log(d.eat());
console.log(d.bark());
console.log(d instanceof Dog, d instanceof Animal);

Output:

Rex eats
Rex barks
true true

Each step matters. Animal.call(this, name) copies own properties onto the new Dog. Object.create(Animal.prototype) makes Dog.prototype delegate to Animal.prototype without invoking Animal. Resetting constructor keeps d.constructor === Dog correct.

A common bug is writing Dog.prototype = new Animal() instead of Object.create(Animal.prototype). That runs the parent constructor prematurely and leaks shared state onto the prototype. Always link prototypes with Object.create.

Classes are sugar over this

ES2015 class syntax produces exactly the same prototype wiring shown above — it is not a new object model. extends sets up the prototype chain, super(...) is the Parent.call(this, ...) step, and methods are placed on the prototype.

class Animal {
  constructor(name) {
    this.name = name;
  }
  eat() {
    return `${this.name} eats`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // calls Animal's constructor with the new `this`
    this.breed = breed;
  }
  bark() {
    return `${this.name} barks`;
  }
}

const d = new Dog("Rex", "Lab");
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.eat(), "/", d.bark());

Output:

true
Rex eats / Rex barks

The first log proves the chain is identical to the manual version: Dog.prototype delegates to Animal.prototype. Classes simply make the intent clearer and handle constructor/super bookkeeping for you.

Comparing the approaches

TechniqueSets up chainRuns parent initBest for
Object.create(proto)YesNo (manual)Plain object delegation, mixins
Constructor + Object.create + callYesYes (via call)Legacy/constructor-style code
class ... extendsYesYes (via super)Modern, readable hierarchies

Best Practices

  • Reach for class ... extends in new code; it generates the correct prototype wiring without manual constructor fixes.
  • Use Object.create(proto) for lightweight delegation when you don’t need a constructor.
  • When writing constructor inheritance by hand, always pair Parent.call(this, ...) with Object.create(Parent.prototype) and restore Dog.prototype.constructor.
  • Keep hierarchies shallow; deep chains slow lookups and make this harder to reason about.
  • Prefer composition (mixing in behavior objects) over deep inheritance trees when relationships aren’t truly “is-a”.
  • Never assign Child.prototype = new Parent() — it runs the parent constructor early and shares mutable state across instances.
Last updated June 1, 2026
Was this helpful?