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.
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.
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); // falseEvery 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.
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 helloEvery 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.
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 livesnpm 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.
A book's title, author, and isbn are different for every single book.
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.
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.
const person = { name: 'Lydia' };
console.log(Object.hasOwn(person, 'toString')); // false
console.log(person.toString()); // '[object Object]', works anywayperson 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
- patterns.dev: Prototype Pattern - covers
Object.createas a second way to build a prototype chain, without a class at all. - Understanding the JavaScript Prototype Chain - a visual walkthrough of the prototype chain using the browser console.
Keep reading