Ch. 1 · JavaScript

Prototypes & Inheritance Without the Headache

How the prototype chain resolves properties, what __proto__ and prototype each mean, what new and class do under the hood, and how to wire up inheritance by hand.

~7 min readintermediate

JavaScript’s inheritance model is simpler than its syntax makes it look. Nothing is copied from a class blueprint into your objects. There are only objects, and each one can point to another object to consult when it doesn’t have a property itself. Everything else, including constructor functions, the new operator, the class keyword and instanceof, is machinery for setting up or inspecting those links. Once you can draw the chain for a given object, prototype questions stop being tricky.

Examples run as ES modules; outputs are from Node 22.

The prototype chain

Every object has an internal slot, [[Prototype]], holding another object or null. To read a property, the engine checks the object’s own properties, then follows [[Prototype]] and checks that object, and so on until it finds the property or reaches null (result: undefined).

const animal = {
  eats: true,
  walk() {
    return `${this.name} walks`;
  },
};

const rabbit = Object.create(animal); // rabbit.[[Prototype]] = animal
rabbit.name = 'Rabbit';

console.log(rabbit.walk()); // → Rabbit walks (found on animal, `this` is rabbit)
console.log(rabbit.eats); // → true
console.log(rabbit.flies); // → undefined (searched the whole chain)

console.log(Object.getPrototypeOf(rabbit) === animal); // → true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // → true
console.log(Object.getPrototypeOf(Object.prototype)); // → null (end of the chain)
JavaScript

The chain here is rabbit → animal → Object.prototype → null. Notice that walk was found on animal but ran with this set to rabbit. The chain decides where a method is found; the call site still decides this (see the this keyword).

Shadowing and writes

An own property with the same name hides, or shadows, the inherited one; delete the own property and the inherited one shows through again. That matters because reads walk the chain but plain assignments do not. Assigning creates or updates an own property, even when the old value came from the prototype:

const proto = { count: 0 };
const a = Object.create(proto);

a.count += 1; // reads 0 through the chain, writes 1 as an *own* property of a

console.log(a.count); // → 1
console.log(proto.count); // → 0 (untouched)
console.log(Object.hasOwn(a, 'count')); // → true
JavaScript

Two exceptions: if the chain has a setter for that name, the setter runs instead, and if it has a non-writable property with that name, the assignment fails (a TypeError in strict mode, silently in sloppy mode).

Gotcha

The write rule doesn’t cover mutation. With Team.prototype.members = [], calling frontend.members.push('Ada') reads the shared array through the chain and mutates it, so backend.members contains 'Ada' too. Keep instance data in the constructor, methods on the prototype.

__proto__ vs prototype

Similar names, different things:

  • [[Prototype]] is the internal link every object has. Read it with Object.getPrototypeOf(obj); set it at creation with Object.create(proto). obj.__proto__ is a legacy accessor for the same link. It lives on Object.prototype, so it only works on objects that inherit from there.
  • prototype is an ordinary property of regular functions and classes: the object that becomes the [[Prototype]] of instances created with new. Instances don’t have one, and neither do arrow functions.
function Dog(name) {
  this.name = name;
}

const rex = new Dog('Rex');

console.log(Object.getPrototypeOf(rex) === Dog.prototype); // → true
console.log(rex.__proto__ === Dog.prototype); // → true (legacy accessor)
console.log(Dog.prototype.constructor === Dog); // → true
console.log(rex.prototype); // → undefined (instances don't get one)
console.log(Object.getPrototypeOf(Dog) === Function.prototype); // → true
JavaScript

Object.create(null) makes an object with no prototype at all (no toString, not even __proto__), which is a safe dictionary for arbitrary keys.

Interview tip

One sentence covers it: “prototype is a property on a constructor; [[Prototype]], exposed as __proto__, is the link on every object; new sets the second to the first.”

Object.create and new, step by step

Object.create(proto) is the most direct tool: it makes an empty object whose [[Prototype]] is proto. The new operator does the same linking as part of a bigger job. new Constructor(...args):

  1. Creates a new empty object.
  2. Sets its [[Prototype]] to Constructor.prototype (or to Object.prototype if that isn’t an object).
  3. Calls Constructor with this bound to the new object.
  4. Returns the new object, unless the constructor explicitly returned a different object.

Interviewers often ask you to implement it:

function myNew(Constructor, ...args) {
  // 1 + 2. Create an object linked to Constructor.prototype
  const proto = Constructor.prototype;
  const obj = Object.create(Object(proto) === proto ? proto : Object.prototype);
  // 3. Run the constructor with `this` = the new object
  const result = Constructor.apply(obj, args);
  // 4. An object returned explicitly wins; primitives are ignored
  return Object(result) === result ? result : obj;
}

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

const ada = myNew(Person, 'Ada');
console.log(ada.greet()); // → Hi, I'm Ada
console.log(ada instanceof Person); // → true
JavaScript

Object(x) === x is a compact “is this an object?” check. Step 4 is the one people forget: a constructor ending in return { b: 2 } makes new produce { b: 2 }, while return 42 is ignored and you get the new object. myNew handles constructor functions only: class constructors throw without new, so for classes you would reach for Reflect.construct.

class is mostly sugar

A class declaration builds the same structure: a constructor function, a prototype object holding the methods, and prototype links for extends.

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
  static create(name) {
    return new this(name);
  }
}

class Dog extends Animal {
  speak() {
    return `${super.speak()}, then barks`;
  }
}

console.log(typeof Animal); // → function
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // → true
console.log(Object.getPrototypeOf(Dog) === Animal); // → true (statics are inherited)
console.log(Dog.create('Rex').speak()); // → Rex makes a sound, then barks
JavaScript

extends sets up two links: Dog.prototype → Animal.prototype for instance methods and Dog → Animal for statics. The “mostly” covers real differences:

  • Calling a class without new throws: Class constructor Animal cannot be invoked without 'new'.
  • Class methods are non-enumerable: Object.keys(Animal.prototype) is [].
  • Class bodies are always strict, and class declarations sit in the TDZ until their line runs (see hoisting and the TDZ).
  • A derived constructor must call super() before touching this, because the base constructor creates the object.
  • Private #fields are not properties, so they never appear on the chain.

Checking the chain

instanceof

value instanceof Constructor asks one question: does Constructor.prototype appear anywhere in value’s prototype chain? It says nothing about which function actually created the object.

function myInstanceOf(value, Constructor) {
  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
    return false; // primitives are never instances
  }
  let proto = Object.getPrototypeOf(value);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

class Animal {}
class Dog extends Animal {}
const rex = new Dog();

console.log(myInstanceOf(rex, Animal), rex instanceof Animal); // → true true
console.log(myInstanceOf(rex, Array), rex instanceof Array); // → false false
console.log(myInstanceOf(5, Number), 5 instanceof Number); // → false false
JavaScript

That definition explains the edge cases. Replace Cat.prototype after creating tom, and tom instanceof Cat becomes false. An array from another iframe fails instanceof Array because it has a different Array.prototype, hence Array.isArray. Classes can customize the check with Symbol.hasInstance.

Own vs inherited properties

const user = Object.create({ role: 'admin' });
user.name = 'Ada';

console.log('role' in user); // → true (checks the whole chain)
console.log(Object.hasOwn(user, 'role')); // → false (own properties only)
console.log(Object.hasOwn(user, 'name')); // → true

const dict = Object.create(null);
dict.key = 1;
console.log(Object.hasOwn(dict, 'key')); // → true
try {
  dict.hasOwnProperty('key');
} catch (e) {
  console.log(e.message); // → dict.hasOwnProperty is not a function
}
JavaScript

hasOwnProperty is itself inherited, so it is missing on null-prototype objects and can be shadowed by an own property. The static Object.hasOwn (ES2022) avoids both problems. Similarly, for...in visits inherited enumerable properties; Object.keys returns own ones only.

Performance notes

  • Engines cache lookups by object shape, so ordinary chain depth costs little. Stable shapes matter more than chain length.
  • Changing an existing object’s prototype (Object.setPrototypeOf, obj.__proto__ = ...) is slow in every major engine and can slow down later code using that object. Create objects with the right prototype up front.
  • Prototype methods are shared: a thousand instances use one speak. Per-instance functions (assigned in the constructor, or arrow class fields) cost one allocation each.
  • Don’t extend built-in prototypes. A library once added Array.prototype.flatten, and the standard method had to ship as flat to avoid breaking sites.

Inheritance without class

A common prompt: “Make Dog inherit from Animal without using class.” There are four steps:

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

function Dog(name, breed) {
  Animal.call(this, name); // 1. run the parent constructor on this object
  this.breed = breed;
}

// 2. Link the prototypes: Dog.prototype → Animal.prototype
Dog.prototype = Object.create(Animal.prototype, {
  // 3. Restore constructor (non-enumerable, like the original)
  constructor: { value: Dog, writable: true, configurable: true },
});

// 4. Override and call the parent method ("super")
Dog.prototype.speak = function () {
  return `${Animal.prototype.speak.call(this)} and barks`;
};
JavaScript
const rex = new Dog('Rex', 'Labrador');

console.log(rex.speak()); // → Rex makes a sound and barks
console.log(rex instanceof Dog, rex instanceof Animal); // → true true
console.log(rex.constructor === Dog); // → true
console.log(Object.keys(rex)); // → [ 'name', 'breed' ]
console.log(Object.keys(Dog.prototype)); // → [ 'speak' ]

Object.setPrototypeOf(Dog, Animal); // optional: inherit statics, like `extends`
JavaScript

The last line is one-time setup on a fresh function, so the performance note above doesn’t apply. Two tempting shortcuts are wrong. Dog.prototype = Animal.prototype makes both constructors share one object, so defining Dog.prototype.speak makes every Animal bark. Dog.prototype = new Animal() runs the parent constructor at setup time, without arguments, and leaves a stray name property on the prototype.

Note

Object.keys(Dog.prototype) lists speak because it was assigned normally; a class method would be non-enumerable. Define it with Object.defineProperty if you need an exact match.

The interview answer

“Every object has an internal [[Prototype]] link to another object or null. Reading a property the object doesn’t have follows that chain until it’s found or the chain ends; assignments create own properties instead. A constructor’s prototype property is the object new links to: new creates an object, sets its [[Prototype]] to Constructor.prototype, runs the constructor with this as that object, and returns it. That’s how instances share methods.

class is mostly syntax over the same mechanism, with stricter rules: it requires new, its methods are non-enumerable, and extends also links the constructors for statics. Without class, I’d call Parent.call(this) in the child constructor, set Child.prototype = Object.create(Parent.prototype), and restore constructor. To inspect the chain I use instanceof and Object.hasOwn.”

More in JavaScript

read ✓JavaScript · mid

Closures, Scope & the Classic setTimeout Loop

What lexical scope and closures really are, why the var + setTimeout loop prints 3 3 3, three ways to fix it, and where closures earn their keep in real code.

~6 min readread →
read ✓JavaScript · hard

The Event Loop: Microtasks vs Macrotasks

How the call stack, task queue and microtask queue fit together, where rendering happens, how async/await schedules work, and output puzzles with answers.

~7 min readread →
esc