Prototype Inheritance Bug — Shared Array Leaks Tenant Data
A shared array on a prototype caused cross-tenant data leakage.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- Prototype chain: each object has a hidden link to another object — property lookup walks up the chain automatically
- __proto__ vs .prototype: __proto__ is instance pointer, .prototype is constructor property that becomes the prototype of instances
- Memory efficiency: one function on prototype saves thousands of copies — ~40 bytes per instance avoided
- Production gotcha: mutating a shared array on prototype affects all instances — use instance properties for mutable state
- Biggest mistake: thinking ES6 classes changed the system — they're still prototypes under the hood
Imagine every employee at a company gets a copy of the employee handbook on their first day. Instead of printing a fresh handbook for every single person, the company pins one master copy to the noticeboard and says 'if you need a rule, check there first.' That noticeboard is the prototype. Your object looks up what it needs, and if it doesn't have the answer itself, it walks up the chain until it finds it — or runs out of noticeboards to check.
JavaScript is one of the few mainstream languages where inheritance isn't bolted on through classes in the traditional sense — it's baked into the very fabric of how objects work. Every single object you create, from a plain {} to a complex class instance, is silently connected to a chain of other objects. Understanding that chain isn't just academic trivia — it's the reason Array methods like .map() and .filter() exist on every array you've ever written without you defining them yourself.
Before ES6 classes arrived, the only way to share behaviour between objects was through prototypes directly. Classes are still prototypes underneath — they're syntactic sugar, not a different system. If you don't understand the prototype chain, you'll hit confusing bugs when properties seem to appear from nowhere, or when methods you thought were isolated suddenly affect every object in your app.
By the end of this article you'll be able to explain exactly what __proto__ and prototype mean and why they're different, build an inheritance hierarchy two ways (prototype-based and class-based) and know the trade-offs, spot and fix the most common prototype bugs, and answer the prototype questions that trip people up in senior JavaScript interviews.
What the Prototype Chain Actually Is — And Why JS Needs It
Every JavaScript object has an internal slot called [[Prototype]]. Think of it as a hidden pointer that says 'if you can't find a property on me, go look over there.' That 'over there' is another object — its prototype — which has its own [[Prototype]], and so on, until you reach Object.prototype, whose [[Prototype]] is null. That's the end of the chain.
This design solves a memory problem. If you create 10,000 User objects and each one stored its own copy of the greet() method, you'd have 10,000 identical functions in memory. With the prototype chain, you define greet() once on User.prototype and all 10,000 instances share a single reference to it. The method lives in one place; the lookup walks there automatically.
The browser exposes this via __proto__ (the instance's pointer to its prototype) and .prototype (a property on constructor functions that becomes the [[Prototype]] of objects they create). These are different things and mixing them up is the source of enormous confusion — we'll untangle that in the code below.
Property lookup always starts on the object itself. Only if the property isn't found there does JS walk up the chain. This means instance properties always shadow prototype properties — a critical detail when debugging unexpected values.
Object.setPrototypeOf() if you must change the prototype at runtime — but prefer Object.create() from the start.Object.getPrototypeOf() for reading, never __proto__.Prototype Chain Hierarchy Visual Diagram
A picture of the prototype chain makes the delegation mechanism immediately clear. The diagram below shows how a simple object created with a constructor function is linked to its prototype, and how that chain eventually reaches Object.prototype and then null.
The chain works like a stack of delegates. When you access a property on an instance, JavaScript first checks the instance itself (own properties). If not found, it checks the immediate prototype. If still not found, it continues up the chain until either the property is found or null is reached. This is why every object has access to methods like toString() and hasOwnProperty() — they live on Object.prototype.
Understanding this visual flow helps debug property resolution order. If you need to see the chain yourself, walk through it programmatically as shown in the code block. The diagrams are mappings of the same concept — the first shows a chain of three objects, the second shows how a class hierarchy looks under the hood.
Implicit Constructors of Literals — How [], {}, and /regex/ Get Their [[Prototype]]
You type []. That's a constructor call. new . Same thing for Array(){} — new . And Object()/foo/ — new RegExp('foo'). Every literal syntax in JavaScript secretly invokes a constructor to set the prototype chain. You get built-in methods for free.
Let's prove it. Object.getPrototypeOf([]) === Array.prototype — true. The empty array's [[Prototype]] points to Array.prototype. That's why [].push(1) works. push lives on Array.prototype. Your literal inherits it.
Same for objects. Object.getPrototypeOf({}) === Object.prototype. true. {}.hasOwnProperty('x') — hasOwnProperty lives on Object.prototype. Primitives get boxed: (42).toString() — the number is boxed to a Number object, whose [[Prototype]] is Number.prototype, which inherits from Object.prototype where toString lives.
Strings: 'hello'.charAt(0) — the string is boxed to a String object, prototype chain: String.prototype → Object.prototype → null. RegExps: /abc/.test('abc') — test lives on RegExp.prototype.
This is why you can call methods on literals. The engine creates an object with the correct [[Prototype]] without you thinking about it. But there's a trap: null and undefined have no prototypes. null is the end of every chain. typeof null === 'object' is a historical bug, but null has no [[Prototype]]. Object.getPrototypeOf(null) throws. throws. null.method()null is not an object — it's the primitive that stops the chain.
Production insight: when you see TypeError: Cannot read properties of undefined, you're trying to call a method on something whose prototype chain doesn't exist. The error message is blunt, but the root cause is often a missing object where you expected one.
One more: Object.create(null) creates an object with no prototype — no toString, no hasOwnProperty. This is useful for dictionaries without key collisions. But you lose all inherited methods. You can't call obj.toString() on it.
Remember: every literal is a constructor invocation in disguise. That's how the prototype chain is wired. [] → Array.prototype → Object.prototype → null. It's automatic. It's invisible. And it's why your array has a map method.
Built-In Types in the Chain — Where Array, Date, and Function Live
Every built-in is an object with a prototype chain. They all end at Object.prototype. But along the way, each type adds its own methods. You need to know where the methods live to understand inheritance and to avoid confusion.
Take an array instance: const arr = [1, 2, 3]. The chain: arr → Array.prototype → Object.prototype → null. Array.prototype holds push, pop, map, filter, reduce, find, forEach, concat, slice, splice, indexOf, forEach, every, some, includes. Over 30 methods. Object.prototype holds toString, hasOwnProperty, valueOf, isPrototypeOf. arr.toString() uses Array.prototype.toString (returns comma-separated), which overrides Object.prototype.toString.
Now a function: function . Chain: f() {}f → Function.prototype → Object.prototype → null. Function.prototype has call, apply, bind. Object.prototype has toString — f.toString() returns the function source. Function.prototype is itself a function — typeof Function.prototype === 'function'. It's the only non-object prototype in the chain. Weird but true.
Date: const d = new → Date()Date.prototype → Object.prototype → null. Date.prototype has getFullYear, getMonth, getDay, getTime, toISOString, toJSON. d.toString() uses Date.prototype.toString (returns date string). toJSON is special — JSON.stringify(d) calls d.toJ which returns an ISO string.SON()
Now, a classic gotcha: Array.isArray is not on Array.prototype. It's a static method on the constructor itself. Array.isArray(arr). Same with Object.keys(obj), Object.defineProperty, Function.prototype methods are accessed via because they're prototype methods.f.call()
You can walk the chain programmatically: Object.getPrototypeOf(arr) gives Array.prototype. Call it again: Object.getPrototypeOf(Array.prototype) gives Object.prototype. One more: Object.getPrototypeOf(Object.prototype) gives null. The chain has a fixed length of at most 3 for built-ins.
Production insight: if you override Array.prototype.toString, you affect JSON.stringify — it calls toString internally. You just broke serialization for every array in your app. Don't do it.
Chain rule: methods closest to the instance are called first. arr.toString → Array.prototype.toString → Object.prototype.toString. The chain is searched upward until a property is found. null stops the search.
Remember: all built-ins eventually reach Object.prototype. That's why arr.hasOwnProperty('length') works — it's inherited from the end of the chain.
The __proto__ vs prototype Distinction Guide
Confusion between __proto__ and .prototype is the single most common source of prototype-related bugs for developers moving from class-based languages to JavaScript. The two terms look similar but serve completely different roles.
__proto__ is the actual [[Prototype]] link on every object instance. It points to the object from which that instance inherits properties. You can read and (in legacy environments) write it, but modern production code should avoid writing it entirely.
.prototype is a property that exists on constructor functions (including classes). It is not the prototype of the function itself — it is the object that will become the [[Prototype]] of all objects created by that constructor when called with new.
Here's the relationship: when you write new Constructor(), JavaScript creates a new object, then sets its [[Prototype]] (accessible via __proto__) to Constructor.prototype. So Constructor.prototype is the blueprint object; instance.__proto__ is the pointer to that blueprint.
This distinction matters because mistakenly assigning to __proto__ instead of .prototype will not set up inheritance for future instances — it only mutates a specific instance's link. And writing to __proto__ in production code can cause V8 deoptimisations because it bypasses inline caching.
Object.create() for new objects or Object.setPrototypeOf() if you absolutely must change the prototype of an existing object — but the performance cost remains.Object.create().Object.getPrototypeOf().Constructor Functions and .prototype — Building Inheritance Before ES6
Before ES6 classes, JavaScript developers used constructor functions to create objects with shared behaviour. A constructor function is just a regular function you call with the new keyword. When you do that, JavaScript does four things automatically: creates a new empty object, sets its [[Prototype]] to the constructor's .prototype property, runs the function body with this pointing to the new object, and returns that object.
This is where the .prototype property on functions becomes important. Every function in JavaScript automatically gets a .prototype object. When you add a method to that .prototype, every instance created by that constructor gets access to it through the chain — without storing their own copy.
The pattern for inheritance between two constructor functions requires one extra step: wiring up the prototype chain manually with Object.create(). If you forget this, instances of the child constructor won't be able to reach the parent's methods. This manual wiring is exactly what ES6 classes automate with the extends keyword — the underlying mechanism is identical.
Understanding this pattern isn't just history — you'll encounter it in older codebases regularly, and knowing it makes ES6 class behaviour completely transparent.
Object.create() gives you a fresh object that delegates to Animal.prototype instead of being it.Object.create() = inheritance.Object.create().ES6 Classes — Same Prototype Chain, Cleaner Syntax
ES6 classes aren't a new object system. They're a cleaner way to write the exact same constructor-function pattern you just saw. Under the hood, class produces a constructor function, and extends sets up the prototype chain using Object.create() just like we did manually. Don't let the keyword fool you into thinking JavaScript became a classical OOP language — it didn't.
The super keyword in a child class does two jobs. Inside the constructor, super(...args) calls the parent's constructor function — the equivalent of Animal.call(this, ...) that we wrote manually. Inside a method, super.methodName() climbs up the prototype chain and calls the parent's version of that method, which is how you extend behaviour rather than replace it.
One genuinely new thing classes give you is private fields (using the # prefix), which the prototype pattern never had. Private fields are stored directly on the instance and are invisible outside the class body — they're not on the prototype at all, which is why they can't be accessed via the chain.
Knowing both syntaxes is what makes you dangerous. You can read legacy code, you can explain what a transpiler like Babel actually outputs, and you'll never be confused by a class behaving 'weirdly' because you understand the prototype reality underneath.
super() in a derived class constructor is a runtime error — JavaScript enforces it because the parent must initialize the instance.super() as the first statement in a derived class, and be explicit about method binding.super.method() calls parent method.Five Ways to Create and Mutate a Prototype Chain
Every object in JavaScript has a [[Prototype]]. Getting it right matters. There are five distinct ways to create or mutate that chain. Each has a specific use case and a specific gotcha. Let’s walk through them.
- Object literals. When you write const obj = { name: 'tcf' }, its [[Prototype]] is Object.prototype automatically. You didn’t do anything. JS did it for you. That’s why obj.toString() works. Gotcha: there’s no way to customise the prototype here. If you need a custom prototype, don’t use a literal.
- Constructor functions. function User(name) { this.name = name; } then User.prototype.login =
function(){ ... }. When you call new User('alice'), the new object’s [[Prototype]] is set to User.prototype. That’s the classic pattern. Gotcha: if you forget new and call User('alice'), you pollute the global object. Always guard with if (!(this instanceof User)) return new User(name). - Object.create(proto). const userProto = {
login(){ ... } }; const alice = Object.create(userProto); alice.name = 'alice'. This is explicit. No constructor. No new. The prototype is exactly what you pass. Gotcha: you must initialise properties manually. Object.create(null) creates an object with no prototype at all — useful for dictionaries. - ES6 class syntax. class User { constructor(name) { this.name = name; }
login(){ ... } }. It’s sugar. The chain is the same. Gotcha: you can’t easily mixin multiple classes. You reach for composition here — which leads us to mixins. - Object.setPrototypeOf(obj, proto). This mutates the prototype at runtime. It’s slow. V8 deoptimises the object. The spec says it’s expensive. Never call it after the object has been used. Use it only for initialising a custom prototype on an object you created. Even then, Object.create is faster.
Here’s a concrete example using io.thecodeforge package:
this in Inherited Methods — The Object Before the Dot Always Wins
You've got a method on a prototype. You call it on an instance. What's this? It's always the object before the dot. Not the prototype where the method lives. This trips up even senior devs. Let's make it concrete.
Take a simple object: const parent = { name: 'Parent', . Now create a child: greet() { console.log(this.name); } }const child = Object.create(parent); child.name = 'Child';. When you call , what prints? 'Child'. The method lives on child.greet()parent, but this is child. The dot's left side wins every time.
Let's chain methods. parent.sayHello = . Now function() { return 'Hello ' + this.name; }; parent.sayGoodbye = function() { console.log(this.sayHello() + ' and goodbye!'); };child.sayGoodbye() works perfectly. this chains through — each call keeps this as child. You get 'Hello Child and goodbye!'. This is why this-aware methods inherited from prototypes work so smoothly.
Now the classic bug. You extract a method: const greet = child.greet;. Call . In strict mode, greet()this is undefined. In sloppy mode, it's the global object. No object before the dot means this is lost. The method travels without its context. This is the number one reason event listeners break. element.addEventListener('click', this.handleClick) — the listener extracts the method, and this becomes the DOM element or undefined. Your callback doesn't work.
The fix: .bind() binds this permanently. const boundGreet = child.greet.bind(child) — now boundGreet() prints 'Child'. Arrow functions also help: const fn = () => . Or in constructors, use the old child.greet()var self = this; or a fat arrow that captures this lexically.
Production story: A team wrote a class for a UI component. The render method called this.fetchData() which was inherited from a base class. They passed render as a callback to a timeout: setTimeout(this.render, 1000). Every single render failed because this was undefined. The fix was one line: setTimeout(this.render.bind(this), 1000). You'll make this mistake once. Then you'll bind everything.
One sharp rule: if you ever pass a method as a callback, bind it before handing it off. Or use an arrow wrapper. Don't assume this travels with the function — it doesn't.
this.method().Writing to the Prototype — Property Shadowing and Setter Interception
You set a property on an object. That property already exists on the prototype. What happens next depends on the property descriptor. There are exactly three outcomes. You need to know all three before you write another line of object-oriented code.
First case: normal data property on the prototype. Assign obj.prop = value. JavaScript creates a new own property directly on obj. The prototype's property is untouched. This is called shadowing — your instance property casts a shadow over the inherited one. obj.hasOwnProperty('prop') becomes true. The prototype's value still exists, but your instance will never see it.
Second case: non-writable data property on the prototype. You used Object.defineProperty(proto, 'prop', { value: 42, writable: false }). Now try obj.prop = 100. In sloppy mode, the assignment silently fails. No own property is created. The value stays 42. In strict mode, you get a TypeError: Cannot assign to read only property 'prop'. This is a production trap. Your code looks correct, but the assignment does nothing. Shadowing is blocked.
Third case: setter on the prototype. Object.defineProperty(proto, 'prop', { set(val) { this._prop = val; } }). Assign obj.prop = 'new'. The setter runs with this set to obj. No own property is created on obj. The setter's side effect happens — in this case, obj._prop becomes 'new'. The prototype's descriptor wins. You can't shadow a setter with a simple assignment.
Let's see all three in code. The non-writable case is the most dangerous. You might expect to override a inherited configuration, but the assignment does nothing. Production bug: a library defines a property as non-writable on SomeConstructor.prototype. Your application code tries to set it on an instance. It silently fails. You spend hours debugging why your component doesn't update.
The fix: always use Object.defineProperty on the instance directly if you need to override a non-writable or setter-defended property. Object.defineProperty(obj, 'prop', { value: yourValue, writable: true }). This bypasses the prototype's descriptor entirely.
One more thing: property descriptors on the prototype affect Object.assign too. Object.assign(obj, { prop: value }) does a Set internally, so it follows the same rules. If the prototype has a non-writable prop, the assignment in Object.assign fails silently too.
Rule to live by: never assume a simple assignment will create an own property. Know the descriptor. Use Object.getOwnPropertyDescriptor(proto, 'prop') before you assign.
Iterating Own vs Inherited Properties — for..in, Object.keys, and hasOwnProperty
You loop over an object. You expect three keys. You get five. You’ve just hit inherited properties. This is one of the most common JavaScript surprises.
for..in iterates ALL enumerable properties, including inherited ones. If you added a method to a shared prototype, for..in will yield it. Here’s the concrete example from a real incident at io.thecodeforge:
Object.keys() and Object.values() return only own enumerable properties. for..in includes both. Use Object.hasOwn(obj, prop) (ES2022) to test ownership.Object.hasOwn() (ES2022) to filter inside for..in.Object.keys() returns only own properties.Object.hasOwn() to filter inside for..in.Monkey Patching Built-In Prototypes — Why You Should Never Touch Array.prototype
You want a method on every array. You think: 'I'll just add it to Array.prototype.' Don't. This is the wrong move. Built-in prototypes are global. Everyone shares them. You don't own them. Touching them is a contract with every piece of code that runs in your process.
Why does it seem attractive? Polyfills. Before Array.prototype.includes was standard, you could add it. But the problem isn't polyfilling a well-defined spec. The problem is adding custom methods. Let's count the ways it breaks.
First: for..in loops. Try for (let key in arr) { console.log(key); }. You get '0', '1', '2', and then — your custom method name. This was a classic jQuery bug in early versions. for..in iterates enumerable own and inherited properties. Adding a method to Array.prototype makes it enumerable by default. Your arrays now have extra keys. Object.keys and for..of are safe, but for..in is not. The web is full of code using for..in on arrays.
Second: conflicts. Library A patches Array.prototype.flatten to return a flat array. Library B patches it to return a string. The order of script loading decides which behaviour wins. You get inconsistent results. You debug for hours. You curse the universe. This is the reason the SmooshGate incident happened. In 2018, TC39 proposed Array.prototype.flatten. MooTools had already patched it with different semantics. Browsers couldn't ship the new method without breaking MooTools sites. The proposal had to be renamed to . One library's monkey patch changed the world's array API.flat()
Third: native platform internals. Node.js and browser engines use prototypes internally. If you patch Object.prototype.hasOwnProperty or Array.prototype.push, you might break engine optimizations or internal calls. The engine assumes the original implementation. Your patch could cause infinite recursion or silent corruption. This is not theoretical — it has happened.
The alternative: standalone utility functions. import { flatten } from './utils.js' — no global state, no conflicts. Or use subclassing: class MyArray extends Array { . Instance methods belong to your instances, not to every array in the universe.flatten() { ... } }
One exception: polyfilling standard methods that are well-specified and not yet implemented. Even then, use a polyfill library like core-js that checks for existence first. Don't write your own.
Rule: if you ever type Array.prototype.something =, stop. Delete the line. Write a utility function instead. Your future colleagues will thank you.
flat() today. Don't be MooTools.Performance Implications of Deep Prototype Chains
Every property access walks the prototype chain. A chain depth of 10 means up to 10 pointer dereferences per access. In a hot loop, that adds up. V8 mitigates this with inline caching (ICs) and hidden classes (“shapes”). But prototype mutation invalidates those caches.
If you call Object.setPrototypeOf(obj, newProto) after obj is already in use, V8 deoptimises the entire function. The next time that function runs, it falls back to a slow path. We measured a ~2x slowdown in a production service at io.thecodeforge after a developer mutated a prototype in a session setup handler.
Keep your chains shallow. Four levels max. If you need more, use composition. Here’s a real example:
Class vs Prototype (Sugar vs Foundation) Comparison Table
One of the most common interview questions is 'Are ES6 classes just syntactic sugar over prototypes?' The short answer is 'mostly yes,' but the full answer requires understanding what the sugar buys you and what you lose. The table below breaks down the exact differences and equivalences between the two patterns.
Both approaches produce a constructor function and set up a prototype chain. The class syntax enforces some rules automatically (strict mode, super() requirement) and adds features that are impossible to replicate exactly (private fields). Conversely, the prototypal pattern allows runtime prototype mutation and mixin composition in ways that class syntax doesn't directly support.
Choosing between them in production often comes down to codebase conventions. If you're working on a modern codebase, prefer class syntax for its readability and safety. If you're maintaining legacy code or need maximum flexibility (e.g., mixing in behaviours at runtime), the prototypal pattern remains useful.
The key insight: both produce objects with the same prototype chain structure. The difference is in the syntax and the enforced semantics, not in the inheritance mechanism itself.
super() calls, and has native private fields. Use prototypal syntax when you need to inherit from a function that isn't a class (e.g., a utility constructor from a library) or when you need to compose mixins dynamically.Real-World Pattern — Composable Mixins Over Deep Inheritance
Here's the dirty secret senior engineers know: deep inheritance hierarchies are fragile. The moment your requirements change, the base class is impossible to modify without breaking every subclass. This is the 'gorilla banana problem' — you wanted a banana, but you inherited the gorilla holding it and the entire jungle it lives in.
The JavaScript prototype chain gives you a practical alternative: mixins. Instead of inheriting from a chain of parent classes, you compose objects by copying or delegating methods from multiple sources. This is idiomatic modern JavaScript and it sidesteps the rigidity of single-parent inheritance entirely.
The most common mixin pattern uses Object.assign() to copy methods onto a prototype, or uses a function that takes a superclass and returns an extended class (a 'class factory mixin'). This lets you snap in capabilities like Serializable, Timestamped, or Auditable without creating a brittle hierarchy.
This matters in the real world because UI component libraries, state management systems, and ORMs all use composition patterns like this. Recognising them — and knowing when to choose composition over inheritance — is what separates intermediate developers from senior ones.
SON(), but they shouldn't share a parent. That's exactly when a mixin earns its keep.Prototype Pollution: The Security Vulnerability You Didn't Know You Had
Prototype pollution is a JavaScript vulnerability where an attacker injects properties into Object.prototype. Since every object inherits from Object.prototype, a single injection can affect all objects in the runtime. This typically happens when recursive merge functions or unsafe JSON parsing overwrite __proto__ or constructor.prototype.
The attack surface is any code that merges user-controlled objects without sanitisation. Libraries like lodash had CVEs for this. The fix is to use Object.create(null) for dictionaries, sanitise property keys, or freeze Object.prototype in high-security environments.
This isn't just a theoretical attack — real breaches have happened via prototype pollution in client-side frameworks and server-side Node.js applications. Understanding the prototype chain is the only way to fully grasp why pollution works and how to prevent it.
Inheritance Is Just Object Linking — There's No Copy, Despite What You Think
Here's where most devs get it wrong: inheritance in JavaScript is not copy. When a child object inherits from a prototype, it doesn't receive a copy of the parent's properties. It gets a live link. That link is [[Prototype]], and it points upward. Property lookup walks the chain every single time. Mutation does not bubble up — you shadow. If you set child.color = 'red', you're not editing the prototype. You're slapping a new own property on child. Now child.color is 'red', and parent.color stays whatever it was. This is why shared mutable state on prototypes cause production bugs. Array.prototype is not a constructor. It's a shared object. Push something onto one array? Fine. Push something onto Array.prototype? Every array in your runtime suddenly has that item. You just broke every for...in loop across your app. The chain is for reading, not writing. Design for that.
this.tasks = [] on a prototype (instead of in the constructor) means every instance shares the same array. One instance pushes, all instances see it. Always initialize mutable objects inside the constructor body.Building Longer Chains Is Usually a Mistake — Here's Why
Three levels deep? Fine. Five? You're debugging. Ten? You're fired. Deep prototype chains kill performance because property lookup is O(depth). Every failed lookup walks the entire chain until it hits null. That's wasted CPU cycles on every property access. Worse, it creates a readability nightmare. user.admin.manager.ceo.salary — which level defines salary? You don't know without Object.getPrototypeOf spamming. Production code stays shallow. Two levels max: a base prototype and one child. If you need more, compose. Mixins, factory functions, or plain object spread — they all beat inheritance once you cross three levels. The built-in chain itself (Object → null) is already two levels. Don't stack more. Your future self debugging a memory leak at 2 AM will thank you.
Factory Functions Beat Prototypes for Composable Behavior
Prototypes are rigid. Once you set the chain, you're locked into a linear hierarchy. Try giving a User object both loggable and serializable behavior. With prototypes, you'd need a diamond inheritance mess or Object.assign hacks. Factory functions solve this without any chain. A factory is just a function that returns an object. Spread mixins, compose behavior, and return plain objects. No constructor, no new, no TypeScript fighting over . Real example: an API client needs logging, caching, and retry logic. Each is a plain object with methods. The factory spreads them into one object. If two mixins collide on a property name, you catch it at merge time, not at runtime. This is how production systems avoid prototype pollution entirely. ES6 classes are fine for UI components. For service objects, data mappers, and middleware? Factories. Every time.super()
Object.create() — The Cleanest Way to Link Prototypes
Forget constructor functions. Forget new. If you want a clean prototype chain, Object.create() is the tool. It does one thing: creates a new object and sets its [[Prototype]] to whatever you pass in. No constructor cruft, no this confusion, no accidental globals.
Here’s the production reality: you often need an object that inherits from another without side effects. Object.create(null) gives you a truly empty dictionary—no prototype pollution, no toString, no hasOwnProperty. Use it for maps or when you’re paranoid about key collisions.
The WHY: Object.create() separates the act of linking from the act of construction. That’s it. No magic. No sugar. Just a plain object with a parent. You want inheritance without ceremony? This is your weapon.
Object.create(null) for hash maps. No inherited keys means no surprise toString in for..in loops. Your colleagues will thank you.Object.create() is the primitive for prototype linking — no constructors, no classes, just inheritance.Object.setPrototypeOf() — The Escape Hatch You Shouldn't Use
You can mutate an object’s prototype after creation with Object.setPrototypeOf(). Does it work? Yes. Should you use it? Almost never. Here’s the WHY: changing an object’s prototype at runtime forces the engine to de-optimize that object and every object sharing its hidden class. Performance tanks. V8 gives up on optimizations for that entire function.
The only production case that justifies it is polyfilling — overriding Array.prototype methods on an array-like object that can’t be rebuilt. That’s it. For everything else, you rebuild the object with Object.create() or assign properties fresh.
Bottom line: Object.setPrototypeOf() is the eval of inheritance. It exists because JavaScript is flexible, not because you’re supposed to use it. If you reach for it, ask yourself hard questions about your architecture first.
Object.setPrototypeOf(), the object is permanently marked as 'slow' in V8. Even deleting the link doesn’t fix it. Prefer Object.create() or a factory function.Object.setPrototypeOf() mutates existing objects and kills engine optimizations — use Object.create() instead unless you’re polyfilling.Inspecting Prototypes — Beyond console.log
Why inspect prototypes? Because debugging inheritance bugs means knowing exactly what exists on an object versus its chain. The simplest tool is Object.getPrototypeOf() — it returns the actual [[Prototype]] without the confusion of __proto__ (which is a getter/setter on Object.prototype, not a true property). For breadcrumb trails, use .constructor.name to identify the constructor behind an instance. For full chain snapshots, walk the chain manually: let proto = Object.getPrototypeOf(obj); while (proto) { console.log(proto.constructor.name); proto = Object.getPrototypeOf(proto); }. Avoid relying on __proto__ in production — it's slow, non-standard, and fails on objects with null prototypes. The real power is combining getPrototypeOf with hasOwnProperty to separate own properties from inherited ones without guesswork.
Object.getPrototypeOf() and hasOwnProperty() for reliable inspection.Object.getPrototypeOf() is the only standard way to read the real prototype without ambiguity.Object.create() — Prototypes Without Constructors
Object.create(proto) builds an object whose [[Prototype]] points directly to proto, bypassing constructors entirely. This gives you pure prototype linking without the baggage of a constructor function or ES6 class. The real gain: you can create objects that inherit from any object — including null (no prototype), which is perfect for plain data maps. Object.create also accepts a second properties descriptor argument to define own properties with fine-grained control (writable, enumerable, configurable). Unlike new Constructor(), there is no initialization step — you get an empty object linked to the prototype. This pattern dominates when you need composable behavior without pretending everything is a class. Use it for mixins, cloning, and when you want an object that truly starts blank.
Object.create() gives you prototype linkage without constructors — perfect for inheritance from plain objects or creating null-prototype maps.The Prototype Pollution That Silently Leaked User Data
- Never put mutable arrays or objects on a prototype — each instance must own its own copy.
- Use
Object.freeze()on prototype properties that should never change. - Audit your codebase for prototype mutations — it's a silent data corruption pattern.
Object.create() in manual inheritance, always reset constructor: Child.prototype.constructor = Child.let obj = someInstance; while (obj) { console.log(obj.constructor?.name, Object.getOwnPropertyNames(obj)); obj = Object.getPrototypeOf(obj); }Or use browser DevTools: console.dir(someInstance) and expand __proto__ chainKey takeaways
Object.create() and 'super()' runs the parent constructor. The chain is identical.Common mistakes to avoid
7 patternsAdding methods directly inside the constructor (this.method = function…)
Forgetting to call super() before using 'this' in a derived class
Mutating a shared array or object on the prototype
Using Dog.prototype = Animal.prototype instead of Object.create
Forgetting to reset constructor after Object.create
Adding a mutable array or object as a property directly on the prototype
Assigning to a property that exists as a non-writable property on the prototype
Interview Questions on This Topic
What is the difference between __proto__ and .prototype in JavaScript? Can you draw out the relationship between a constructor function, its .prototype property, and an instance created from it?
Foo() {} → Foo.prototype is an object; let f = new Foo() → f.__proto__ === Foo.prototype. This is the relationship: constructor → [.prototype] → prototype object → [__proto__] → instances.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's Advanced JS. Mark it forged?
23 min read · try the examples if you haven't