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

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__andprototype? - Rewrite this
class Dog extends Animalwithout theclasskeyword. - Why did changing one instance's array change every instance?
- How does
instanceofdecide its answer? - Is
classin 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.prototypeproperty? - 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
extendscreates (instance side and static side)? - Do you know
instanceofchecks the prototype chain, not "which constructor made this"? - Can you say what
classadds beyond sugar instead of reciting "it is just syntax"?
Core mental model: one hidden link, followed on lookup
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.
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; // trueNotice 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..prototypeis an ordinary property that constructor functions and classes have. It holds the object thatnewwill 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.
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.
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 DogLookup 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:
- Instance side:
Dog.prototype's[[Prototype]]isAnimal.prototype, so instances inherit methods. - Static side:
Dog's own[[Prototype]]isAnimal, so static methods likecreateare 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:
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
TypeErrorif called withoutnew.Dog("Rex")on the function version silently runs with the wrongthis. - Class methods are non-enumerable, so they do not show up in
for...in. Methods assigned toDog.prototypeby hand are enumerable. - Class bodies are always strict mode code.
- A derived constructor must call
super()before it touchesthis. The parent constructor creates the object, which is whyclass MyList extends Arraygets a real array while theArray.call(this)pattern does not. superworks through the home object, and private#fieldsexist 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.
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.
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 chainClassic 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?"
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 fromTwo 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.
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:
- Every object has a hidden
[[Prototype]]link to another object ornull. Reading a missing property walks that chain until it finds the key or hitsnull. .prototypeis a property on constructors.new C()installsC.prototypeas the new object's[[Prototype]], so methods there are shared by all instances.class Dog extends AnimallinksDog.prototypetoAnimal.prototype(instance methods) andDogtoAnimal(statics).superstarts lookup one level above the method's home object.- 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.
classis mostly syntax over the same chain, plus real guarantees:newrequired, non-enumerable methods, strict mode,super()beforethis, 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__andprototypeare the same thing." No.[[Prototype]](exposed by__proto__) is the link on every object..prototypeis 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.
- "
classis a completely different object model from prototypes." No. Same chain, same lookup. - "
classis purely syntax sugar with no differences." Also no. Calling withoutnewthrows, methods are non-enumerable, bodies are strict, and derivedthisis created bysuper(). - "
instanceofchecks which constructor created the object." No. It checks whetherC.prototypeis on the chain. - "Prototypes are legacy, nobody uses them now." No. Every
classruns on them. Preferclassin 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, thenObject.prototype, thennull. [[Prototype]]is the link;.prototypeis what a constructor installs as that link on new instances.extendswires two chains: instance methods and statics.- Reads walk the chain; writes land on the receiver. Keep mutable state per instance.
instanceofis a chain check, so it can surprise you across realms or after prototype reassignment.classis 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.