The Prototype pattern — or why a shared Google Doc link beats emailing everyone a PDF
You don't email everyone a PDF of the spec — you send a link, and when you fix a typo in it, everyone who already had that link sees the fix too, not just people who open it after you edited. JavaScript's Prototype pattern works the same way: instead of stamping the same behavior onto every object, objects share a live link to it, and look it up the moment they're asked. Here it is, in one small, real example.
The issue
You don't email everyone a PDF of the spec. You send a link — because a PDF freezes the moment you attach it, so if you fix a typo in it tomorrow, everyone who already has that PDF is still reading yesterday's version. A link doesn't have that problem: open it today, open it next week, you're always looking at whatever's actually there right now. That's the whole idea behind JavaScript's Prototype pattern: don't stamp a private copy of some behavior onto every object you create — give them all a live link to one shared object, and let them look things up there the moment they're asked.
Say your dashboard pops up a little toast every time something happens — a payment lands, a server restarts. Every toast needs the same two behaviors: show() and dismiss(). The obvious way to build that is a factory function that returns a fresh object, methods and all, every single time.
The naive version
Nothing exotic — just a function that builds a toast and hands it back:
function createToast(message) {
return {
message,
seen: false,
show() {
console.log("Toast: " + this.message);
},
dismiss() {
this.seen = true;
},
};
}
const t1 = createToast("Payment received");
const t2 = createToast("Server restarted");
t1.show === t2.show; // false — two separate functions doing the exact same thingThat last line is the problem, just made visible. t1 and t2 don't share show — each one got handed its own private copy, built fresh inside createToast the moment it was created. You could ask whether that wastes memory, and honestly: a little, and that's the least interesting thing wrong with it. The real problem is that there is now no such thing as "what a toast does." There's only whatever each individual toast happened to be handed at birth. Change dismiss() tomorrow and every toast already in existence keeps running the old one — because the old one is inside it. There's no shared place to reach in and fix them.
The duplicated functions do cost bytes, and at a few thousand rows that genuinely starts to matter — but that's a different pattern's job. If what you're fighting is the same data repeated across a long list, Issue 023 is the one you want. This one is about something else: having one live answer to "what can this kind of thing do," and being able to change that answer.
The fix: one class, one shared prototype
This is exactly what class is for — and it's worth being precise about what it does, because the syntax makes it look like something else. A class body is not a template that gets stamped onto every object it makes:
class Toast {
constructor(message) {
this.message = message;
this.seen = false;
}
show() {
console.log("Toast: " + this.message);
}
dismiss() {
this.seen = true;
}
}
const t1 = new Toast("Payment received");
const t2 = new Toast("Server restarted");
t1.show === t2.show; // true — one function, shared by every toast
Object.getPrototypeOf(t1) === Toast.prototype; // true — and this is where it actually livesOnly the constructor runs once per toast, and it's the only part that puts anything on the toast itself — message and seen. show and dismiss get attached exactly once, to a single object that came into existence alongside the class: Toast.prototype. Every new Toast(...) gets a live link to that object — JavaScript calls the link [[Prototype]] — rather than a copy of what's on it. One definition of what a toast does, in one place, that every toast asks at the moment it's called.
So when you call t1.dismiss(), the engine checks t1 first, doesn't find a dismiss sitting there, follows the link up to Toast.prototype, finds it, and runs it — with this still pointing at t1, which is how it manages to set t1's own seen. That isn't a special rule for methods, either: it's the same lookup that runs every time you read any property JavaScript can't find directly on an object.
The live link — and an honest note about it
Say dismiss() was supposed to log that the toast got dismissed, and nobody wrote that part. There's exactly one object to fix:
Toast.prototype.dismiss = function () {
this.seen = true;
sendAnalytics("toast_dismissed"); // the behavior that was missing
};
t1.dismiss(); // runs the NEW dismiss — and t1 was created long before this line existedt1 was never holding its own dismiss to begin with — every call was always going to walk up to Toast.prototype and run whatever is there at that moment. Patch that one object and every toast in existence behaves differently on its next call.
Worth being straight about that, though: you are almost certainly never going to do it. Code changes when you ship a deploy, not while the process is running — nobody patches a live method to fix a bug. The demo is how you see the link. It isn't the reason to use it.
The real reason: you're using it either way
This isn't a technique you opt into. Every object already delegates — an empty one included:
const plain = {};
plain.toString; // a function you never wrote
Object.getPrototypeOf(plain) === Object.prototype; // true — it was there all alongSo the only real choice is whether your objects point at a prototype that says something useful, or at the empty default with your methods hand-copied on beside the data. A class is just the first option, spelled the way everyone expects.
The memory side is real too, and it's less about bytes than people assume. Every method in the naive factory is a fresh closure per toast, and a closure holds its whole scope alive — whatever createToast happened to have in hand can't be collected for as long as that toast lives. Prototype methods are built once, close over nothing, and leave each instance holding only its own data.
And the rest of the language already assumes you did it this way: t1 instanceof Toast answers truthfully, { ...t1 } gives you clean data instead of two stray functions, extends costs one keyword, and a test can stub Toast.prototype.show once to cover every toast the code creates.
Before class existed, you wrote this by hand
class only landed in JavaScript in 2015. The prototype underneath it has been there since 1995 — which means for the first two decades of the language, people built this exact pattern with nothing but functions, and you'll still walk into that code in older files and libraries. Two shapes come up, and both do precisely what the class above does.
The first is the constructor function — an ordinary function you call with new, with the shared methods hung off its .prototype one at a time:
function Toast(message) {
this.message = message;
this.seen = false;
}
Toast.prototype.show = function () {
console.log("Toast: " + this.message);
};
Toast.prototype.dismiss = function () {
this.seen = true;
};
const t1 = new Toast("Payment received");
const t2 = new Toast("Server restarted");
t1.show === t2.show; // true — the same shared function as the class versionThat is, almost literally, what class desugars to. Same Toast.prototype, same [[Prototype]] link on every instance, same live lookup — the class version just writes it as one block instead of four statements, and throws in a couple of guardrails: calling Toast() without new throws instead of quietly writing to the global object, and the methods are non-enumerable, so they stay out of for...in loops.
The second shape skips new altogether. Build the shared object yourself, then hand each toast a link to it with Object.create:
const toastPrototype = {
show() {
console.log("Toast: " + this.message);
},
dismiss() {
this.seen = true;
},
};
function createToast(message) {
const toast = Object.create(toastPrototype); // a link, not a copy
toast.message = message;
toast.seen = false;
return toast;
}
const t1 = createToast("Payment received");
const t2 = createToast("Server restarted");
t1.show === t2.show; // true — shared again, and not a new in sightObject.create(toastPrototype) is the pattern with nothing on top of it — no new, no constructor, no class keyword, just "make me an object, and point it at this one." It's the clearest way to see what's really going on, and it stays genuinely useful when the thing you want to share behavior from is an object you already have rather than a class you're about to define. For everyday code, though, write the class: it says the same thing with less ceremony, and it's what the next person reading the file expects to find.
Where a shared Prototype can bite you
A shared Prototype isn't about being clever with memory — it's a single, inspectable place that decides what every instance of a thing can do, instead of that decision getting stamped onto each one separately the moment it's built. Reach for it whenever you're about to write the exact same method into a hundred near-identical objects. And remember it isn't an exotic technique you have to opt into: every class you've ever written was already doing it.
For the rest of the prototype chain — Object.getPrototypeOf, setPrototypeOf, and how far this goes — patterns.dev's Prototype Pattern writeup is a good next stop.