Intermediate 5 min · March 05, 2026

Prototype Inheritance Bug — Shared Array Leaks Tenant Data

A shared array on a prototype caused cross-tenant data leakage.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • 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

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.

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.

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.

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.

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.

Constructor Functions vs ES6 Classes
AspectConstructor Functions (.prototype)ES6 Classes (class / extends)
Syntax clarityVerbose — chain wiring is manualClean — extends and super handle it
Prototype chainIdentical underlying mechanismIdentical underlying mechanism
Private stateRequires closure workaroundNative private fields with # prefix
Calling parent constructorAnimal.call(this, ...args)super(...args) — must be first
Calling parent methodAnimal.prototype.method.call(this)super.method()
HoistingFunction declarations are hoistedClasses are NOT hoisted — use after declaration
Strict modeOptionalAlways in strict mode automatically
Seen in codebasesLegacy and transpiled output (Babel)Modern JS, TypeScript, frameworks

Key Takeaways

  • The prototype chain is a linked list of objects — property lookup walks up it automatically, which is why .map() exists on every array without you defining it.
  • Constructor function .prototype and instance __proto__ are different things: .prototype is a property on functions that becomes the [[Prototype]] of instances; __proto__ (or Object.getPrototypeOf) is how you read that link on an instance.
  • ES6 classes compile down to the same constructor-function + prototype pattern — 'extends' runs Object.create() and 'super()' runs the parent constructor. The chain is identical.
  • Deep inheritance hierarchies are fragile — class factory mixins let you compose reusable abilities (Serialisable, Timestamped) onto unrelated classes without forcing them into a single hierarchy.
  • Prototype pollution is a real security threat — understanding the chain is the first step to preventing it.

Common Mistakes to Avoid

  • Adding methods directly inside the constructor (this.method = function…)
    Symptom: Every instance carries its own copy of the function, wasting memory. Debug by checking instance.hasOwnProperty('method') returns true.
    Fix: Move methods to the prototype: ClassName.prototype.method = function… or use ES6 class syntax where methods are automatically on the prototype.
  • Forgetting to call super() before using 'this' in a derived class
    Symptom: ReferenceError: Must call super constructor in derived class before accessing 'this'
    Fix: Always call super(...args) as the first statement in the child class constructor. JavaScript enforces this because the parent constructor initializes the object.
  • Mutating a shared array or object on the prototype
    Symptom: Pushing to one instance's array updates every instance's array — looks like a ghost-write bug.
    Fix: Initialize arrays and objects inside the constructor (this.items = []) so each instance gets its own copy. Prototypes should only hold methods, never mutable state.
  • Using Dog.prototype = Animal.prototype instead of Object.create
    Symptom: Adding a method to Dog.prototype also adds it to Animal.prototype — because they're the same object.
    Fix: Use Dog.prototype = Object.create(Animal.prototype) to create a new object that delegates to Animal.prototype.
  • Forgetting to reset constructor after Object.create
    Symptom: instance.constructor points to the parent class, not the child. Breaks code that relies on constructor for type checks.
    Fix: After setting prototype, add: Child.prototype.constructor = Child;

Interview Questions on This Topic

  • QWhat 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?JuniorReveal
    __proto__ is the actual [[Prototype]] link on an instance — it points to the object that the instance inherits from. .prototype is a property that exists on constructor functions (and class declarations). When you use 'new', the newly created object's [[Prototype]] is set to the constructor's .prototype. So: function Foo() {} → Foo.prototype is an object; let f = new Foo() → f.__proto__ === Foo.prototype. This is the relationship: constructor → [.prototype] → prototype object → [__proto__] → instances.
  • QWhat does the 'new' keyword actually do step by step? If you had to implement your own version of 'new' as a function, how would you write it?Mid-levelReveal
    The new keyword does four things: 1) Creates a new empty object. 2) Sets the object's [[Prototype]] to the constructor's .prototype property. 3) Executes the constructor function with 'this' bound to the new object. 4) Returns the new object (unless the constructor returns a non-primitive object, in which case that object is returned instead). A simple implementation: function myNew(Constructor, ...args) { const obj = Object.create(Constructor.prototype); const result = Constructor.apply(obj, args); return (typeof result === 'object' && result !== null) ? result : obj; }
  • QES6 classes are often called 'syntactic sugar over prototypes' — do you agree with that statement entirely, or are there things classes do that the old constructor-function pattern genuinely cannot?SeniorReveal
    I mostly agree — classes use the same prototype chain under the hood. However, there is one genuine addition: private fields (#). The old constructor pattern could achieve something similar with closures and WeakMaps, but it was verbose and not syntactically enforced. Private fields in classes are a real language feature that cannot be replicated exactly by the prototype system alone because they have true encapsulation enforced by the engine. Also, class syntax automatically runs in strict mode, which the old pattern did not enforce. So while the inheritance mechanism is the same, classes add syntactic and encapsulation improvements.
  • QExplain prototype pollution — what is it, how does it work, and how do you prevent it?SeniorReveal
    Prototype pollution is a vulnerability where an attacker injects properties into Object.prototype. Since all objects inherit from Object.prototype, the injected property becomes available on every object in the runtime. It works by exploiting code that recursively merges objects without sanitizing keys like __proto__, constructor, or prototype. For example, a deep merge of attacker-controlled JSON can traverse up the prototype chain and set a property directly on Object.prototype. Prevention: use Object.create(null) for maps, strip __proto__ and constructor keys during merge, freeze Object.prototype in security-critical environments, and avoid unsafe recursive merges.

Frequently Asked Questions

What is the prototype chain in JavaScript?

The prototype chain is the mechanism JavaScript uses to look up properties and methods. Every object has a hidden [[Prototype]] link to another object. When you access a property, JS first checks the object itself, then walks up the chain through each linked object until it either finds the property or reaches null at the end of the chain.

What is the difference between classical inheritance and prototypal inheritance?

Classical inheritance (Java, C++) copies a blueprint — the class — into new instances at compile time. Prototypal inheritance (JavaScript) links objects together at runtime. Instances don't copy methods; they delegate to their prototype. This means you can modify the prototype after instances are created and all instances immediately see the change.

Does using ES6 classes mean JavaScript is no longer prototype-based?

No. ES6 classes are syntax on top of the same prototype system. Running class Dog extends Animal creates a constructor function and wires Dog.prototype to Animal.prototype using Object.create() — exactly as you'd do manually. You can verify this by checking Object.getPrototypeOf(Dog.prototype) === Animal.prototype, which returns true for any class hierarchy.

What is the best way to inspect an object's prototype chain?

Use Object.getPrototypeOf() in a loop until null. Or in browser DevTools, console.dir(obj) shows the full chain. Avoid __proto__ in production code.

How do I prevent prototype pollution in my application?

Use Object.create(null) for objects that serve as maps, always filter __proto__ and constructor keys when merging user input, freeze Object.prototype in critical environments, and use libraries with built-in pollution prevention (e.g., lodash's cloneDeep has patches but still sanitize yourself).

🔥

That's Advanced JS. Mark it forged?

5 min read · try the examples if you haven't

Previous
Event Loop in JavaScript
5 / 27 · Advanced JS
Next
ES6+ Features in JavaScript