JavaScript's Prototype Chain: One Shared Copy for Every Instance

How JavaScript's own prototype chain lets every instance share one copy of a method instead of getting its own, and the multi-level inheritance trap that makes tracing a property's origin harder than it looks.

September 7, 20264 min read2 / 2

This isn't the Gang-of-Four Prototype pattern, which is about cloning an existing object as a template. It's the mechanism JavaScript itself is built on: the prototype chain. The last post in this detour ended on a different kind of waste: not a leaked variable, but a method rebuilt from scratch every time an identical object gets created. Here's the idea in one sentence: every method defined on a class body lives on one shared object called the prototype, so every instance points to the exact same function instead of getting its own copy.

Proving the Waste, Then Fixing It

Say you build many robots with a plain function that returns an object, a technique for manufacturing objects to a fixed shape without ever declaring a class.

JavaScript
function createRobot(name) { return { name, wave() { console.log(`${name} waves hello`); }, }; } const zed = createRobot('Zed'); const bolt = createRobot('Bolt'); console.log(zed.wave === bolt.wave); // false

Every single call to createRobot builds a brand new wave function, even though the code inside it never changes. With two robots that costs nothing you'd notice. With ten thousand, you're holding ten thousand separate copies of an identical function in memory, for no reason other than how they were created.

A class fixes this without changing anything about how you use it.

JavaScript
class Robot { constructor(name) { this.name = name; } wave() { console.log(`${this.name} waves hello`); } } const zed2 = new Robot('Zed'); const bolt2 = new Robot('Bolt'); console.log(zed2.wave === bolt2.wave); // true zed2.wave(); // Zed waves hello

Every method written directly in a class body, wave here, gets attached once to a shared object called Robot.prototype, not copied onto each instance. zed2 and bolt2 both have their own name, since that's set fresh in the constructor for each one, but they point at the exact same wave function. Ten thousand robots would still mean exactly one wave function in memory, not ten thousand.

JavaScript
console.log(Object.hasOwn(zed2, 'wave')); // false, it's not zed2's own property console.log(Object.hasOwn(Robot.prototype, 'wave')); // true, this is where it actually lives
ℹ️ Run this yourself: the plain-function version and the class-based prototype version are in the code-practice repo. Clone it, npm install, and check starter/index.js's comment for the exact refactor to make before comparing against final/.

When There's Nothing to Share

Not every object-building function has this problem. Say createBook(title, author, isbn) returns { title, author, isbn }, only unique data, genuinely different on every book.

✕
All-unique data

A book's title, author, and isbn are different for every single book.

✓
Shared behavior

A wave or bark method runs identical code no matter which instance calls it.

A prototype only pays off when something is actually identical across every instance. Moving unique data to a shared prototype wouldn't just fail to save memory, it would be a bug: every book would end up reading the same title.

The Trap: A Deep Chain Hides Where Things Come From

Classes can extend classes, and each level only has to define what's new.

JavaScript
class Employee { constructor(name, employeeId) { this.name = name; this.employeeId = employeeId; } } class Manager extends Employee { approveTimeOff() { console.log(`${this.name} approved time off`); } } class Executive extends Manager { signContract() { console.log(`${this.name} signed the contract`); } } const casey = new Executive('Casey', 'E-1042'); casey.approveTimeOff(); // works, defined two levels up casey.signContract(); // works, defined on Executive itself console.log(casey.employeeId); // 'E-1042'

Executive never mentions employeeId anywhere. It reaches casey because Executive's constructor runs Manager's constructor, which runs Employee's constructor, and that's where this.employeeId = employeeId actually happens. approveTimeOff isn't copied down to Executive either, it's found by walking up from casey to Executive.prototype to Manager.prototype, where the search stops because that's where the method actually lives.

That walk is the exact same mechanism that makes prototypes memory-efficient, and the exact same mechanism that makes a deep chain confusing. Three levels here was already enough to ask "wait, where does employeeId actually come from?" Five or six levels deep in a real codebase, that question gets a lot harder to answer.

You've Already Used This Without Knowing It

Every plain object literal you've ever written already does this.

JavaScript
const person = { name: 'Lydia' }; console.log(Object.hasOwn(person, 'toString')); // false console.log(person.toString()); // '[object Object]', works anyway

person never defined toString. JavaScript gives every object a hidden link to Object.prototype, which is where toString, valueOf, and a handful of other methods actually live. That's the same prototype chain from the Employee example above, just one you've been relying on since your very first object literal.

That wraps up this detour into what JavaScript already builds into the language itself. The main Gang-of-Four sequence continues with the classic pattern catalog this series is working through one at a time.

Reference