Writing · MARGIN NOTESFront-End Development

What Interviewers Mean When They Ask About Prototypes

JavaScript classes still run on prototype chains. Here is the interview mental model for [[Prototype]] vs .prototype, property lookup, what extends really wires, super, instanceof, and the gotchas that trip candidates.

Author
Nicola Riker Urdaneta
Published
October 6, 2026
Reading time
7 min read
What Interviewers Mean When They Ask About Prototypes
Contents

"How does inheritance work in JavaScript?" sounds like a warm-up question. It is usually a trap for candidates who learned class and extends first and never looked underneath. Interviewers want to see whether you can say where a method actually lives, how the engine finds it, and what extends changes at runtime.

The model is small. Every object has a hidden link to another object (or null). Property lookup follows that link. class is mostly a clearer way to build those links, plus a few guarantees that hand-written constructor functions never had. Once you can draw the chain, super, instanceof, and most "why is this shared?" bugs explain themselves.

Why interviewers ask

The prompt usually looks like one of these:

  • What is the difference between __proto__ and prototype?
  • Rewrite this class Dog extends Animal without the class keyword.
  • Why did changing one instance's array change every instance?
  • How does instanceof decide its answer?
  • Is class in JavaScript "real" inheritance or just sugar?

They are testing five things at once:

  • Can you separate an object's internal [[Prototype]] link from a function's .prototype property?
  • Do you know lookup walks the chain on read, but plain assignment writes an own property on the receiver?
  • Can you name the two links extends creates (instance side and static side)?
  • Do you know instanceof checks the prototype chain, not "which constructor made this"?
  • Can you say what class adds beyond sugar instead of reciting "it is just syntax"?

Every ordinary object has an internal slot called [[Prototype]]. It points to another object or to null. You read it with Object.getPrototypeOf(obj) and choose it at creation time with Object.create(proto).

When you read obj.key, the engine checks the object's own properties first. If the key is missing, it moves to [[Prototype]] and checks there, then that object's [[Prototype]], until it finds the key or reaches null (result: undefined). That walk is the prototype chain.

JavaScript
const animal = {
  speak() {
    return `${this.name} makes a sound`;
  },
};

const dog = Object.create(animal); // dog's [[Prototype]] is animal
dog.name = "Rex";

dog.speak(); // "Rex makes a sound": found on animal, called with this === dog
Object.hasOwn(dog, "speak"); // false: not an own property
"speak" in dog; // true: the in operator walks the chain
Object.getPrototypeOf(dog) === animal; // true

Notice the split: lookup found speak on animal, but this is still dog. The chain decides where a method is found. The call site decides this. Keep those two questions separate and most inheritance code reads cleanly.

__proto__ is an old accessor on Object.prototype that reads and writes the same hidden link. It is kept for web compatibility. Prefer Object.getPrototypeOf and Object.create in code you write, and say so in the interview.

prototype vs [[Prototype]]: the question behind the question

This is the confusion interviewers are fishing for. They are two different things:

  • [[Prototype]] is the hidden link every object has. It is what lookup follows.
  • .prototype is an ordinary property that constructor functions and classes have. It holds the object that new will install as the [[Prototype]] of each instance.

A function's own [[Prototype]] is Function.prototype. Its .prototype property is a separate object meant for its instances.

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

const a = new Animal("Milo");

Object.getPrototypeOf(a) === Animal.prototype; // true: the instance link
Object.getPrototypeOf(Animal) === Function.prototype; // true: the function's own link
Animal.prototype.constructor === Animal; // true: default back-pointer
a.speak(); // "Milo makes a sound"

Say it in one line: new Animal() creates an object whose [[Prototype]] is Animal.prototype, then runs Animal with this bound to that object. Methods put on Animal.prototype exist once and are shared by every instance through lookup.

Arrow functions and method shorthand have no .prototype property and cannot be called with new. Only constructors get one.

What class and extends actually wire

Here is the same hierarchy in class syntax. The comments name the links the engine creates.

JavaScript
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()} (woof)`;
  }
}

const d = new Dog("Rex");
d.speak(); // "Rex makes a sound (woof)"

Object.getPrototypeOf(d) === Dog.prototype; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true: instance-side link
Object.getPrototypeOf(Dog) === Animal; // true: static-side link
Dog.create("Bo") instanceof Dog; // true: static method inherited, this is Dog

Lookup for d.speak walks d → Dog.prototype → Animal.prototype → Object.prototype → null and stops at the first match, which is Dog.prototype.speak. Overriding is just finding a closer property first.

extends creates two links, and strong candidates name both:

  1. Instance side: Dog.prototype's [[Prototype]] is Animal.prototype, so instances inherit methods.
  2. Static side: Dog's own [[Prototype]] is Animal, so static methods like create are inherited too.

super.speak() is not "look up speak on this's parent." A method remembers the object it was defined on (its home object, here Dog.prototype), and super starts lookup at that object's [[Prototype]], which is Animal.prototype. It still calls the method with the current this. That fixed starting point is why super does not loop forever in a three-level hierarchy.

Rewrite it without class

Interviewers often ask for the pre-class version to see whether you know what the sugar does. This is the honest translation:

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

function Dog(name) {
  Animal.call(this, name); // run the parent's setup on this object
}
Dog.prototype = Object.create(Animal.prototype); // instance-side link
Dog.prototype.constructor = Dog; // restore the back-pointer we just replaced
Object.setPrototypeOf(Dog, Animal); // static-side link

Dog.prototype.speak = function () {
  return `${Animal.prototype.speak.call(this)} (woof)`;
};

const d = new Dog("Rex");
d.speak(); // "Rex makes a sound (woof)"

The method lookup and the chain are the same. That is why people call class sugar. It is mostly sugar, but not only sugar. class also gives you:

  • Class constructors throw a TypeError if called without new. Dog("Rex") on the function version silently runs with the wrong this.
  • Class methods are non-enumerable, so they do not show up in for...in. Methods assigned to Dog.prototype by hand are enumerable.
  • Class bodies are always strict mode code.
  • A derived constructor must call super() before it touches this. The parent constructor creates the object, which is why class MyList extends Array gets a real array while the Array.call(this) pattern does not.
  • super works through the home object, and private #fields exist only in class syntax.

The interview-ready phrasing: "Classes use the same prototype chain. They add safer defaults and a few capabilities the old pattern could not express."

Classic gotcha: shared state on the prototype

Lookup on read plus own-property creation on write produces the most common prototype bug. Put a mutable object on the prototype and every instance shares it until one of them assigns over it.

JavaScript
function Team() {}
Team.prototype.members = []; // one array, shared by every instance

const a = new Team();
const b = new Team();

a.members.push("Nicola"); // reads members through the chain, mutates the shared array
b.members; // ["Nicola"]: same array

a.members = ["Sam"]; // assignment creates an own property on a (shadowing)
b.members; // still ["Nicola"]

push never assigns a.members. It reads the inherited array and mutates it. Assignment is different: it writes an own property on a that shadows the inherited one. (The exceptions are an inherited setter, which gets called instead, and an inherited non-writable property, which blocks the write.)

The fix is to create per-instance state in the constructor or with a class field, and keep only shared behavior on the prototype.

JavaScript
class Counter {
  count = 0; // class field: an own property on each instance
  inc() {
    // method: one shared function on Counter.prototype
    this.count += 1;
  }
}

const c1 = new Counter();
const c2 = new Counter();

Object.hasOwn(c1, "count"); // true
Object.hasOwn(c1, "inc"); // false
c1.inc === c2.inc; // true: same function, found through the chain

Classic gotcha: instanceof checks the chain

x instanceof C does not ask "did C construct x?" By default it asks "does C.prototype appear anywhere on x's prototype chain?"

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

d instanceof Dog; // true
d instanceof Animal; // true: Animal.prototype is on d's chain
d instanceof Object; // true

const bare = Object.create(null); // [[Prototype]] is null
bare instanceof Object; // false: Object.prototype is not on its chain
// bare.toString(); // TypeError: nothing to inherit toString from

Two follow-ups interviewers like. If a constructor function's .prototype is reassigned after instances exist, those old instances stop passing instanceof for it (a class's .prototype is non-writable, so this bites the function pattern). And objects from another realm (an iframe, for example) have a different Array.prototype, so instanceof Array can be false for a real array. That is why Array.isArray exists.

Classic gotcha: this before super

In a derived class, this does not exist until super() returns, because the parent constructor is the one that creates the object.

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

class Dog extends Animal {
  constructor(name) {
    // this.tricks = []; // ReferenceError: must call super() before using this
    super(name);
    this.tricks = []; // fine: the object exists now
  }
}

If a derived class has no constructor, JavaScript supplies one that forwards all arguments to super(...args). You only write a constructor when you need extra setup.

What to say out loud in an interview

A strong answer sounds like this:

  1. Every object has a hidden [[Prototype]] link to another object or null. Reading a missing property walks that chain until it finds the key or hits null.
  2. .prototype is a property on constructors. new C() installs C.prototype as the new object's [[Prototype]], so methods there are shared by all instances.
  3. class Dog extends Animal links Dog.prototype to Animal.prototype (instance methods) and Dog to Animal (statics). super starts lookup one level above the method's home object.
  4. Reads walk the chain. Plain assignment creates an own property on the receiver and shadows the inherited one, which is why mutable state belongs on the instance.
  5. class is mostly syntax over the same chain, plus real guarantees: new required, non-enumerable methods, strict mode, super() before this, and private fields.

Then draw d → Dog.prototype → Animal.prototype → Object.prototype → null and point at where each method lives.

Common wrong answers to avoid

  • "__proto__ and prototype are the same thing." No. [[Prototype]] (exposed by __proto__) is the link on every object. .prototype is the object a constructor hands to its instances.
  • "Methods are copied onto each instance." No. Prototype methods exist once and are found through lookup. Only class fields and constructor assignments create per-instance properties.
  • "class is a completely different object model from prototypes." No. Same chain, same lookup.
  • "class is purely syntax sugar with no differences." Also no. Calling without new throws, methods are non-enumerable, bodies are strict, and derived this is created by super().
  • "instanceof checks which constructor created the object." No. It checks whether C.prototype is on the chain.
  • "Prototypes are legacy, nobody uses them now." No. Every class runs on them. Prefer class in app code, and understand the chain it builds.
  • "Assigning to an inherited property changes it for everyone." No. Plain assignment shadows it with an own property. Mutating an inherited object (like push) is what changes it for everyone.

Takeaways

  • Draw the chain first: instance, then C.prototype, then the parent's .prototype, then Object.prototype, then null.
  • [[Prototype]] is the link; .prototype is what a constructor installs as that link on new instances.
  • extends wires two chains: instance methods and statics.
  • Reads walk the chain; writes land on the receiver. Keep mutable state per instance.
  • instanceof is a chain check, so it can surprise you across realms or after prototype reassignment.
  • class is the right default in app code. Knowing what it builds is what interviewers are checking.

If you can explain [[Prototype]] vs .prototype, rewrite a small extends hierarchy by hand, and predict where a method is found, you are interview-ready on prototypes.