On this page · sections
JavaScript Prototypes, Classes & Inheritance
Can you explain
newas a protocol, and inheritance as prototype lookup — not as class copying?
JavaScript does not copy methods from a class into every object.
Objects have an internal [[Prototype]] link. When a property is not found on the object itself, JavaScript can delegate the lookup to that prototype, continuing up the chain until it finds the property or reaches null.
class provides cleaner syntax for constructing and extending these relationships, but the underlying object model remains prototype-based.
The senior-level mental model is:
Object
↓
own properties
↓
[[Prototype]]
↓
prototype object
↓
its [[Prototype]]
↓
...
↓
nullThese questions test:
- The four conceptual steps of
new [[Prototype]]vs.prototype- Prototype-chain lookup
instanceofclass,extends, andsuper- Own properties vs shared prototype methods
- Constructor return behavior
- Composition vs inheritance
Questions
Construction
Classes
Senior-Level Follow-ups
Construction
1. What new Actually Does
Why This Matters
If you understand the new protocol, much of JavaScript’s constructor behavior becomes predictable.
It also gives you the mental model behind:
- constructor functions
classextends- prototype linkage
instanceof
Problem
What happens when you write:
new Constructor(arg);The new Protocol
Conceptually, JavaScript does four things:
1. Create a new object
2. Set its [[Prototype]] to Constructor.prototype
3. Call Constructor with the new object as this
4. Determine the result:
- returned object → use it
- otherwise → return the new objectFor example:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function greet() {
return `Hi, ${this.name}`;
};
const alice = new Person("Ada");Now:
alice.name;
// "Ada"
alice.greet();
// "Hi, Ada"
Object.getPrototypeOf(alice) === Person.prototype;
// trueThe important relationship is:
alice
│
└── [[Prototype]]
↓
Person.prototype
↓
Object.prototype
↓
nullgreet is not copied into alice.
When JavaScript evaluates:
alice.greetit first looks for greet on alice.
It doesn’t find it.
It then follows:
alice.[[Prototype]]which points to Person.prototype, where greet is found.
What Happens Without new?
Consider:
"use strict";
function User() {
this.id = 1;
}
User();There is no constructor-created receiver.
Because the function is running in strict mode:
this === undefined;Therefore assigning:
this.id = 1;throws a TypeError.
Classes make this distinction explicit:
class User {}
User();
// TypeError: Class constructor User cannot be invoked without 'new'A class constructor is not an ordinary function that can simply be called.
Interview Answer
“
newconceptually creates an object, links its[[Prototype]]to the constructor’s.prototype, invokes the constructor with that object asthis, and returns the constructed object unless the constructor explicitly returns another object. The important part is that the prototype is linked, not copied.”
2. Prototype Chain Lookup
Why This Matters
JavaScript inheritance is fundamentally property lookup through delegation.
That is different from class-based systems where developers often imagine inherited members being copied into the instance.
Problem
Given:
const animal = {
eats: true,
walk() {
return "walking";
},
};
const rabbit = {
jumps: true,
};
Object.setPrototypeOf(rabbit, animal);What happens when we access:
rabbit.jumps;
rabbit.eats;
rabbit.walk;Lookup
JavaScript effectively searches:
rabbit
↓
animal
↓
Object.prototype
↓
nullTherefore:
rabbit.jumps;
// trueThe property is found directly on rabbit.
But:
rabbit.eats;
// trueis found on animal.
And:
rabbit.walk();
// "walking"finds the function on animal.
Own Properties vs Prototype Properties
You can distinguish them with:
Object.hasOwn(rabbit, "jumps");
// true
Object.hasOwn(rabbit, "eats");
// falseThis distinction is important when reasoning about:
- serialization
- property ownership
- mutation
- performance
- inheritance
- security-sensitive property checks
Writes Do Not Usually Mutate the Prototype
Suppose:
rabbit.eats = false;JavaScript creates an own property on rabbit:
rabbit
├── jumps: true
└── eats: false
animal
└── eats: trueNow:
rabbit.eats;
// false
animal.eats;
// trueThe new own property shadows the inherited property.
This is one of the most important prototype rules:
Read
→ search own object
→ then prototype chain
Write
→ normally create/update an own propertyThere are advanced exceptions involving accessors/setters and special object semantics, so “writes always create an own property” is a useful default model, not an absolute language rule.
[[Prototype]] vs .prototype
This distinction causes a huge number of interview mistakes.
[[Prototype]]
Every ordinary object has an internal [[Prototype]] link.
You inspect it with:
Object.getPrototypeOf(obj);For:
const alice = new Person();you get:
Object.getPrototypeOf(alice) === Person.prototype;
// true.prototype
.prototype is a property found on constructable functions and is the object used as the default prototype for instances created by that constructor.
So:
Person
│
└── .prototype
↓
Person.prototype
↑
│ [[Prototype]]
aliceThese are different concepts.
alice.prototype;
// undefined
Person.prototype;
// objectThe instance has [[Prototype]].
The constructor function has .prototype.
__proto__
__proto__ is a legacy accessor for an object’s [[Prototype]].
Prefer:
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, proto);for explicit prototype operations.
Also, avoid repeatedly changing prototypes of live objects in performance-sensitive code. Prefer constructing objects with the intended prototype from the beginning.
Interview Answer
“
[[Prototype]]is the internal delegation link on an object.Constructor.prototypeis an object associated with a constructor and is normally used as the prototype for objects created withnew. Property reads walk the chain; missing properties are delegated upward. Writes normally create or update own properties and can shadow inherited ones.”
Classes
3. class, extends, and super
Why This Matters
class does not turn JavaScript into a classical copy-based inheritance system.
It provides syntax for defining constructors, prototype methods, inheritance, and related behavior on top of JavaScript’s existing object model.
Problem
How does this work?
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${super.speak()} barks`;
}
}Instance Chain
dog
↓
Dog.prototype
↓
Animal.prototype
↓
Object.prototype
↓
nullTherefore:
const dog = new Dog("Rex", "Lab");
dog.speak();JavaScript first looks for speak on:
dogthen:
Dog.prototypewhere it finds the overridden method.
super()
Inside a derived constructor:
class Dog extends Animal {
constructor(name) {
super(name);
}
}super(name) invokes the parent constructor.
A derived constructor cannot use this before super():
class Dog extends Animal {
constructor(name) {
this.name = name;
super(name);
}
}This throws.
The correct order is:
class Dog extends Animal {
constructor(name) {
super(name);
this.name = name;
}
}The parent constructor establishes the this value for the derived constructor.
super.method()
super is not simply “the parent object.”
Consider:
class Dog extends Animal {
speak() {
return `${super.speak()} barks`;
}
}super.speak() performs a lookup starting from the parent prototype.
It then calls the method with the current receiver as this.
That distinction matters:
super.speak()
↓
lookup begins on Animal.prototype
↓
method executes with current Dog instance as thisIt does not mean:
this = Animal.prototypeStatic Inheritance
There are actually two relevant chains with extends.
For instances:
Dog.prototype
↓
Animal.prototype
↓
Object.prototypeFor constructors themselves:
Dog
↓
Animal
↓
Function.prototypeThis is why static methods can also be inherited:
class Animal {
static category() {
return "animal";
}
}
class Dog extends Animal {}
Dog.category();
// "animal"Fields vs Methods
Instance fields become own properties:
class User {
name = "Ada";
greet() {
return `Hi ${this.name}`;
}
}Conceptually:
user
├── name ← own property
└── [[Prototype]]
↓
User.prototype
└── greet() ← shared methodPrivate fields are also per-instance state:
class Account {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
}#balance is not an ordinary public property and cannot be accessed as:
account.#balance;outside the class body.
Interview Answer
“
classstill uses prototypes.extendsestablishes both an instance prototype relationship and a static inheritance relationship. A derived constructor must callsuper()before usingthis. Prototype methods are shared, while instance fields and private fields represent per-instance state.”
4. instanceof and Realms
Why This Matters
instanceof is not simply asking:
“Was this object created by this constructor?”
It is fundamentally a prototype-chain test.
Problem
What does this check?
alice instanceof Person;Given:
const alice = new Person("Ada");JavaScript checks whether:
Person.prototypeappears somewhere in:
alice
↓
alice.[[Prototype]]
↓
...Therefore:
alice instanceof Person;
// trueAnd:
alice instanceof Object;
// truebecause Object.prototype also appears on the chain.
Conceptually:
alice
↓
Person.prototype ← found
↓
Object.prototype
↓
nullinstanceof and Cross-Realm Objects
Browsers can have multiple JavaScript realms, for example:
- the main window
- an iframe
- another document
Each realm has its own built-in constructors.
That means:
window.Array !== iframe.contentWindow.Array;An array created in the iframe can therefore fail:
value instanceof Array;when checked against the main window’s Array.
For arrays, prefer:
Array.isArray(value);because it correctly identifies arrays across realms.
For general built-in type identification, Object.prototype.toString.call can also be useful:
Object.prototype.toString.call(value);
// "[object Date]"
// "[object Array]"
// etc.Interview Answer
“
instanceofchecks whether a constructor’s.prototypeappears on the object’s prototype chain. It is therefore sensitive to realms because each realm has separate constructor objects. For arrays crossing realm boundaries,Array.isArrayis the safer check.”
Senior-Level Follow-ups
5. Why Methods Live on the Prototype
The Problem With Per-Instance Methods
Consider:
function BadUser(name) {
this.name = name;
this.greet = function () {
return `Hi ${this.name}`;
};
}Every instance receives a separate function object:
user1 → greet function #1
user2 → greet function #2
user3 → greet function #3The implementation is identical, but the function objects are different.
Prototype Method
Instead:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Hi ${this.name}`;
};Now:
user1 ─┐
user2 ─┼──→ User.prototype.greet
user3 ─┘The instances share the same method.
This is the normal reason class methods live on the prototype:
class User {
greet() {}
}is conceptually:
User.prototype
└── greetrather than creating a fresh greet function for every instance.
Arrow Class Fields
This pattern is different:
class User {
greet = () => {
return this.name;
};
}greet becomes an own property of each instance.
That can be useful because the arrow captures the instance’s lexical this:
const user = new User();
const fn = user.greet;
fn();
// still uses the instanceBut it means each instance gets its own function.
So the trade-off is:
Prototype method
→ shared function
→ dynamic this
Arrow class field
→ own function per instance
→ lexical thisNeither is universally “better.”
Choose based on the semantics you need.
Interview Answer
“Prototype methods are shared by instances, avoiding a separate function object per instance. Arrow class fields trade that sharing for lexical
this, which can be convenient for callbacks and event handlers.”
6. Returning an Object From a Constructor
Why This Matters
A constructor can override the object that new returns.
Consider:
function Broken() {
this.value = 42;
return {
hijacked: true,
};
}
const result = new Broken();The returned object wins:
result;
// { hijacked: true }The initially created object containing:
value: 42is not returned.
What If the Constructor Returns a Primitive?
function User() {
this.name = "Ada";
return 123;
}
const user = new User();
user.name;
// "Ada"A primitive return value is ignored.
The practical rule is:
Constructor returns object
→ returned object wins
Constructor returns primitive
→ constructed object wins
Constructor returns nothing
→ constructed object winsThis also explains why:
const result = new Broken();
result instanceof Broken;
// falseThe returned object was not constructed with Broken.prototype as its prototype.
Interview Answer
“A constructor invoked with
newcan replace the constructed result by returning an object. A primitive return value is ignored. If an object is returned, the result may no longer have the constructor’s prototype, soinstanceofcan become false.”
7. Composition vs Inheritance
Why This Matters
Understanding prototypes does not mean every design should use extends.
Deep inheritance trees create coupling between:
- base classes
- subclasses
- lifecycle assumptions
- overridden methods
- constructor behavior
Frontend systems frequently benefit from composition and delegation instead.
Inheritance
Inheritance models:
“A
Dogis anAnimal.”
class Animal {
speak() {}
}
class Dog extends Animal {
bark() {}
}This can be appropriate when the subtype relationship is genuine and the shared behavior belongs naturally in the base abstraction.
Composition
Composition models:
“This object uses these capabilities.”
For example:
function createLogger() {
return {
log(message) {
console.log(message);
},
};
}
function createService(logger) {
return {
run() {
logger.log("running");
},
};
}Now the service depends on a capability rather than a particular superclass.
You can substitute another implementation:
const logger = {
log(message) {
// send to remote logging service
},
};
const service = createService(logger);This is often easier to test and replace.
Practical Rule
Use inheritance when:
There is a genuine subtype relationship
+
Shared behavior belongs naturally to the parent
+
The inheritance contract is stablePrefer composition when:
Behavior should be interchangeable
+
Dependencies should be injected
+
You want smaller independent units
+
The relationship is "uses" rather than "is a"Frontend architecture often favors composition because UI behavior tends to be assembled from independent capabilities rather than rigid class hierarchies.
Interview Answer
“Inheritance is useful when there is a stable subtype relationship and shared behavior naturally belongs to the base abstraction. For interchangeable behavior and frontend composition, I generally prefer delegation, dependency injection, and composition because they reduce coupling.”
Final Interview Strategy
When you see a prototype question, draw the chain.
1. What object are we starting with?
↓
2. Which own properties does it have?
↓
3. What is its [[Prototype]]?
↓
4. Where is the requested property found?
↓
5. If it is a constructor, what is Constructor.prototype?
↓
6. If new is involved, what object becomes this?
↓
7. If instanceof is involved, does Constructor.prototype
appear on the object's chain?Keep these two concepts separate:
Constructor.prototype
↓
object used as the default prototype for new instances
instance.[[Prototype]]
↓
the instance's actual prototype linkAnd remember:
class
↓
clean syntax
↓
prototype-based object modelQuick Reference
| Concept | Mental Model |
|---|---|
obj.[[Prototype]] | Object’s internal delegation link |
Fn.prototype | Prototype object associated with a constructable function |
new Fn() | Create → prototype-link → call → return |
| Property read | Own property → prototype chain |
| Property write | Normally creates/updates an own property |
class | Syntax built around JavaScript’s prototype model |
extends | Establishes prototype inheritance relationships |
super() | Invokes the parent constructor |
super.method() | Looks up starting from the parent prototype |
instanceof | Tests whether Fn.prototype is on the object’s chain |
| Class method | Usually shared through the prototype |
| Instance field | Own property |
| Arrow class field | Own function with lexical this |
| Constructor returns object | Returned object replaces constructed result |
| Constructor returns primitive | Primitive is ignored |
Senior-Level Signal
A weak answer is:
“
classis how JavaScript does inheritance.”
A stronger answer is:
“JavaScript inheritance is prototype-based. Objects delegate property lookup through
[[Prototype]], whileclassprovides syntax for constructing and linking those objects.”
A senior answer continues:
“
newis a construction protocol: it creates a receiver, establishes its prototype, invokes the constructor with that receiver, and determines whether the constructor’s return value replaces it.instanceofis then a prototype-chain question, not simply a constructor-history question.”
The most useful diagram to remember is:
const user = new User();
user
│
│ [[Prototype]]
▼
User.prototype
│
│ [[Prototype]]
▼
Object.prototype
│
│ [[Prototype]]
▼
nullAnd the key distinction:
JavaScript inheritance is lookup and delegation — not copying.