Prototypes and inheritance

JavaScript · Advanced · 15 min

Prototypes explain how classes, methods and instanceof actually work, and why some objects behave unexpectedly.

Every object has an internal link, [[Prototype]], to another object (or null). When you read a property that the object doesn't own, the engine looks it up on the prototype, then the prototype's prototype, and so on — the prototype chain — until it finds it or reaches null and returns undefined.

Writing is different: assigning obj.x = 1 creates or updates an own property on obj (unless the chain has a setter), shadowing any inherited x.

Inspect and set the link with Object.getPrototypeOf(obj) and Object.create(proto). You'll also see obj.__proto__; it's a legacy accessor — avoid it in new code. Object.setPrototypeOf exists but changing a live object's prototype is slow.

Classes are built on this. For class Dog extends Animal:

  • methods are stored once on Dog.prototype, shared by every instance;
  • each instance's [[Prototype]] is Dog.prototype;
  • Dog.prototype's [[Prototype]] is Animal.prototype, then Object.prototype, then null;
  • static members live on the constructor, and Dog's own prototype is Animal.

instanceof simply checks whether Ctor.prototype appears in the object's chain. Built-ins work the same way: [].map is found on Array.prototype.

Two practical consequences:

  • Object.hasOwn(obj, key) distinguishes own from inherited properties; in and for...in see inherited ones too.
  • Plain {} inherits from Object.prototype, so "constructor" in {} is true. For a pure dictionary use Object.create(null) — or a Map.

Never add methods to built-in prototypes such as Array.prototype in application code; it collides with future standard methods and other libraries.

Example

const animal = {
  speak() { return `${this.name} makes a sound`; },
};
const dog = Object.create(animal);         // dog -> animal -> Object.prototype
dog.name = "Rex";
console.log(dog.speak());                   // Rex makes a sound (inherited)
console.log(Object.hasOwn(dog, "speak"));   // false

dog.speak = function () { return "Woof"; }; // own property shadows
console.log(dog.speak(), animal.speak.call(dog)); // Woof Rex makes a sound

class Shape { area() { return 0; } }
class Square extends Shape {
  constructor(s) { super(); this.s = s; }
  area() { return this.s ** 2; }
}
const sq = new Square(3);
console.log(Object.getPrototypeOf(sq) === Square.prototype);          // true
console.log(Object.getPrototypeOf(Square.prototype) === Shape.prototype); // true
console.log(Object.hasOwn(sq, "area"), sq instanceof Shape);         // false true

const dict = Object.create(null);           // no inherited keys at all
console.log("toString" in dict, "toString" in {}); // false true

Remember

  • Reads walk the prototype chain; writes create own properties
  • Class methods live on Class.prototype, shared by all instances
  • instanceof checks whether Ctor.prototype is in the chain

Watch out for

  • Extending built-in prototypes like Array.prototype in app code.

Check yourself

  1. Where does the method area live for instances of class Square { area() {} }?
  2. const p = { x: 1 }; const o = Object.create(p); o.x = 2; — what is p.x afterwards?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain