Topics
v6.0.0
On this page · sections

JavaScript Prototypes, Classes & Inheritance

Can you explain new as 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]] ↓ ... ↓ null

These questions test:

  • The four conceptual steps of new
  • [[Prototype]] vs .prototype
  • Prototype-chain lookup
  • instanceof
  • class, extends, and super
  • Own properties vs shared prototype methods
  • Constructor return behavior
  • Composition vs inheritance

Questions

Construction

  1. What new Actually Does
  2. Prototype Chain Lookup

Classes

  1. class, extends, and super
  2. instanceof and Realms

Senior-Level Follow-ups

  1. Why Methods Live on the Prototype
  2. Returning an Object From a Constructor
  3. Composition vs Inheritance

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
  • class
  • extends
  • 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 object

For 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; // true

The important relationship is:

alice │ └── [[Prototype]] ↓ Person.prototype ↓ Object.prototype ↓ null

greet is not copied into alice.

When JavaScript evaluates:

alice.greet

it 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

“new conceptually creates an object, links its [[Prototype]] to the constructor’s .prototype, invokes the constructor with that object as this, 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 ↓ null

Therefore:

rabbit.jumps; // true

The property is found directly on rabbit.

But:

rabbit.eats; // true

is 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"); // false

This 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: true

Now:

rabbit.eats; // false animal.eats; // true

The 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 property

There 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]] alice

These are different concepts.

alice.prototype; // undefined Person.prototype; // object

The 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.prototype is an object associated with a constructor and is normally used as the prototype for objects created with new. 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 ↓ null

Therefore:

const dog = new Dog("Rex", "Lab"); dog.speak();

JavaScript first looks for speak on:

dog

then:

Dog.prototype

where 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 this

It does not mean:

this = Animal.prototype

Static Inheritance

There are actually two relevant chains with extends.

For instances:

Dog.prototype ↓ Animal.prototype ↓ Object.prototype

For constructors themselves:

Dog ↓ Animal ↓ Function.prototype

This 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 method

Private 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

“class still uses prototypes. extends establishes both an instance prototype relationship and a static inheritance relationship. A derived constructor must call super() before using this. 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.prototype

appears somewhere in:

alice ↓ alice.[[Prototype]] ↓ ...

Therefore:

alice instanceof Person; // true

And:

alice instanceof Object; // true

because Object.prototype also appears on the chain.

Conceptually:

alice ↓ Person.prototype ← found ↓ Object.prototype ↓ null

instanceof 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

“instanceof checks whether a constructor’s .prototype appears 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.isArray is 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 #3

The 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 └── greet

rather 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 instance

But 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 this

Neither 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: 42

is 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 wins

This also explains why:

const result = new Broken(); result instanceof Broken; // false

The returned object was not constructed with Broken.prototype as its prototype.

Interview Answer

“A constructor invoked with new can 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, so instanceof can 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 Dog is an Animal.”

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 stable

Prefer 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 link

And remember:

class ↓ clean syntax ↓ prototype-based object model

Quick Reference

ConceptMental Model
obj.[[Prototype]]Object’s internal delegation link
Fn.prototypePrototype object associated with a constructable function
new Fn()Create → prototype-link → call → return
Property readOwn property → prototype chain
Property writeNormally creates/updates an own property
classSyntax built around JavaScript’s prototype model
extendsEstablishes prototype inheritance relationships
super()Invokes the parent constructor
super.method()Looks up starting from the parent prototype
instanceofTests whether Fn.prototype is on the object’s chain
Class methodUsually shared through the prototype
Instance fieldOwn property
Arrow class fieldOwn function with lexical this
Constructor returns objectReturned object replaces constructed result
Constructor returns primitivePrimitive is ignored

Senior-Level Signal

A weak answer is:

“class is how JavaScript does inheritance.”

A stronger answer is:

“JavaScript inheritance is prototype-based. Objects delegate property lookup through [[Prototype]], while class provides syntax for constructing and linking those objects.”

A senior answer continues:

“new is 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. instanceof is 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]] ▼ null

And the key distinction:

JavaScript inheritance is lookup and delegation — not copying.

Last updated on