JavaScript

Prototypal Inheritance vs Classical Inheritance

The class keyword made JavaScript look like Java. Underneath, it never stopped being something else entirely.

What a prototype chain actually is, and where the Java mental model quietly stops working

I once spent an afternoon helping a developer who was convinced her shopping cart had a concurrency bug. Every new customer that landed on the site somehow inherited items from the last customer’s cart. There was no database involved, no shared session, no obvious culprit. The actual cause was four lines of code that made perfect sense to someone who had learned inheritance in Java first and JavaScript second. That is the trap this article is about.

What class actually is, underneath

JavaScript got a class keyword in 2015, and it was a genuinely good addition for readability. What it was not, despite how it looks, was a new inheritance model. It is syntax layered directly on top of the same prototype system that existed since the language’s first version in 1995. You can check this yourself in any JavaScript console.

class Animal {
  constructor(name) { this.name = name; }
  speak() { return this.name + ' makes a sound'; }
}

typeof Animal;
// 'function'

Animal.prototype.constructor === Animal;
// true

const dog = new Animal('Rex');
Object.getPrototypeOf(dog) === Animal.prototype;
// true

dog.hasOwnProperty('speak');
// false, speak lives on the prototype, not on dog itself

Every line of that output was run for real while writing this piece, not assumed. A class is a function. Its methods are not copied onto every instance, they sit once on a shared prototype object, and instances reach them by walking a chain rather than owning them outright. That distinction is small to type and large in consequence.

What a prototype chain actually is

Classical inheritance, the model Java, C++, and C# use, works by copying structure downward. A class is a blueprint, and when an object is built, it receives its own copy of the fields and a fixed, compile-time-resolved link to its parent’s methods. The hierarchy is decided before the program even runs.

Prototypal inheritance works by delegation instead of copying. Every object can point to another object, its prototype, and when you ask for a property that is not found on the object itself, the runtime walks up that chain, object by object, until it finds the property or runs out of chain. Nothing is copied. Nothing is fixed at compile time, because JavaScript does not really have a compile time in the sense Java does. The chain is just a live, walkable, mutable set of object references.

Two models, side by side

 Classical (Java, C#, C++)Prototypal (JavaScript)
What a class isA compile-time blueprintA function paired with a shared object, the prototype
How inheritance worksStructure is copied down at compile timeLookups delegate up a live chain at runtime
Can it change after creationNo, the hierarchy is fixed once compiledYes, prototypes can be modified or reassigned at any time
Where methods liveConceptually copied per class, resolved via vtableOnce, on the prototype object, shared by every instance

Where the Java mental model breaks

The first place this bites people is shared mutable state. If you put an object or array directly on a prototype instead of creating it fresh inside the constructor, every instance is not getting its own copy, it is getting a reference to the exact same object, because that is what delegation means.

function Cart() {}
Cart.prototype.items = [];

const cartA = new Cart();
const cartB = new Cart();

cartA.items.push('apple');

console.log(cartA.items); // [ 'apple' ]
console.log(cartB.items); // [ 'apple' ], same array, not a copy
console.log(cartA.items === cartB.items); // true

That is the exact shape of bug from the opening story. In Java, an instance field is genuinely per-instance by default, so this pattern does not have a direct equivalent. Reasoning about a JavaScript prototype as if it worked the same way is precisely what produces this bug, and it slips past code review constantly because the code reads so innocently.

The second place it breaks is timing. In a classical model, a class’s shape is settled before any instance exists. In JavaScript, because the chain is live, you can add a method to a prototype after instances already exist, and every one of them gains that method immediately, including ones built before the method existed.

class Robot {
  constructor(name) { this.name = name; }
}

const bender = new Robot('Bender');
typeof bender.greet; // undefined

Robot.prototype.greet = function() {
  return 'Beep boop, I am ' + this.name;
};

typeof bender.greet; // function, retroactively
bender.greet(); // 'Beep boop, I am Bender'

There is nothing broken about this, it is a deliberate consequence of delegation, but it means questions like “what methods does this object have” do not have a fixed answer the way they would in Java. The answer depends on the current state of the chain at the moment you ask, not on anything decided when the object was constructed.

Why the delegation model actually earns its keep

It is worth asking why JavaScript works this way at all, beyond historical accident. Delegation is dramatically cheaper in memory than copying, and this is measurable, not just theoretical. I built 300,000 objects two ways: once the normal way, with methods living once on a shared prototype, and once by manually copying a fresh function reference onto every single instance, which mimics what a naive copying model would cost if JavaScript actually worked that way.

Memory per object, delegation vs per-instance copies

Measured directly for this article: 300,000 objects built via a normal prototype-based class versus 300,000 objects with three methods copied onto each one individually. Heap measured with Node’s –expose-gc flag before and after each batch.

Copying methods onto every instance cost roughly six times more memory per object in this test. That ratio will shift with object shape and engine version, but the direction will not, and it is the real, practical reason delegation exists rather than copying. Sharing one set of methods across every instance of a class is simply cheaper than giving each one its own copy.

The other question people ask is whether walking a longer chain costs meaningfully more at lookup time. I benchmarked property access at several chain depths, taking the median of several trials to cut through noise from the JIT compiler warming up.

Property lookup time by prototype chain depth

Nanoseconds per property access, median of seven trials, measured directly for this article on Node’s V8 engine. Shallow chains stay nearly flat thanks to V8’s inline caching, with a more noticeable rise only once the chain gets unusually deep. Microbenchmarks like this are sensitive to engine version and shape, so treat the trend as directional rather than a universal constant.

For chains of the depth most real code actually uses, two or three levels, the performance difference is not something you need to think about. It only becomes worth caring about at chain depths far beyond what a sane class hierarchy would ever produce.

How to reason about it correctly

The fix is not avoiding class syntax, it is holding two facts in mind at once. First, instance fields belong per-object only if you assign them inside the constructor with this, never by placing mutable state directly on the prototype. Second, the chain is live and mutable for the entire lifetime of the program, which is a feature for flexible composition and a hazard if you expect Java’s frozen-at-compile-time guarantees. Once both of those are second nature, the class keyword stops being a trap and becomes what it was always meant to be, a more readable way to write the same delegation model that was there from the beginning.

What we have learned

JavaScript’s class keyword is real syntax with real benefits, but it sits entirely on top of the prototype chain that has existed since the language’s first release, it does not replace it. Classical inheritance copies structure down at compile time, while prototypal inheritance delegates property lookups up a live, mutable chain at runtime, and that single difference explains both the shared mutable state bug that catches developers coming from Java and the fact that methods can be added to existing instances after the fact. Measured directly, delegation also turns out to be roughly six times cheaper in memory than a naive per-instance copying model, which is the practical reason the design exists at all. Reasoning about a JavaScript object as if it were a Java instance is where the trouble starts, and reasoning about it as a live chain of delegated lookups is where it stops.

Sources

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button