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.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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
Prototype inheritance is JavaScript's mechanism for object-to-object delegation, where objects inherit properties and methods from other objects through a chain of internal [[Prototype]] references. Unlike classical inheritance in languages like Java or C++, JS doesn't copy behavior—it delegates failed property lookups up the chain.
This design exists because JavaScript was built in 10 days for Netscape, needed to be lightweight, and had to handle dynamic object manipulation without a class system. Every object in JS has a prototype (except Object.create(null)), and when you access obj.prop, the engine walks the chain until it finds the property or hits null.
This is why [].map() works—arrays delegate to Array.prototype, which delegates to Object.prototype, then null.
In practice, the prototype chain is the backbone of JS's entire type system. Literals like [], {}, and /regex/ get their prototypes implicitly through built-in constructors: [] gets Array.prototype, {} gets Object.prototype, /regex/ gets RegExp.prototype.
These constructors are functions with a .prototype property that becomes the [[Prototype]] of all instances. The distinction between __proto__ (the actual chain link on an instance) and .prototype (the template object on a constructor) is a common source of confusion—__proto__ is the runtime reference, .prototype is the blueprint.
Before ES6 classes, developers built inheritance by manually setting Child.prototype = Object.create(Parent.prototype) and adjusting constructor references.
The critical bug this article addresses arises when shared arrays or objects live on a prototype rather than on instances. If you define this.items = [] inside a constructor, each instance gets its own array. But if you put items: [] on the prototype, all instances share the same array—mutations by one tenant leak data to others.
This is not a JS bug but a misunderstanding of delegation: prototypes are for shared methods, not shared mutable state. In multi-tenant SaaS apps or server-side rendering, this pattern causes data cross-contamination that's notoriously hard to debug because it only manifests under concurrent access patterns.
The fix is always to initialize mutable state in the constructor, not on the prototype.
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
// --- Demonstrating the prototype chain from scratch --- // A plain object — no constructor, no class const vehicleBlueprint = { describe() { // 'this' refers to whatever object called the method return `I am a ${this.type} that travels at ${this.topSpeed} km/h`; } }; // Create a new object whose prototype IS vehicleBlueprint const bicycle = Object.create(vehicleBlueprint); bicycle.type = 'bicycle'; // own property — lives directly on bicycle bicycle.topSpeed = 30; // own property const sportsCar = Object.create(vehicleBlueprint); sportsCar.type = 'sports car'; sportsCar.topSpeed = 300; // Both objects share ONE describe() function — it lives on vehicleBlueprint console.log(bicycle.describe()); // JS looks on bicycle first — no describe() there // then walks up to vehicleBlueprint — found it! console.log(sportsCar.describe()); // Confirm the chain console.log(Object.getPrototypeOf(bicycle) === vehicleBlueprint); // true console.log(Object.getPrototypeOf(vehicleBlueprint) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype)); // null — end of chain // Checking OWN properties vs inherited properties console.log(bicycle.hasOwnProperty('type')); // true — own property console.log(bicycle.hasOwnProperty('describe')); // false — inherited from vehicleBlueprint
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.
// Walk the prototype chain and log each layer function walkChain(obj) { let current = obj; let depth = 0; while (current !== null) { console.log(`Depth ${depth}: ${current.constructor?.name || '[Null]'}`); console.log(' Own properties:', Object.getOwnPropertyNames(current)); current = Object.getPrototypeOf(current); depth++; } console.log('End of chain'); } // Example with a User instance function User(name) { this.name = name; } User.prototype.greet = function() { return `Hi ${this.name}`; }; const u = new User('Alice'); walkChain(u); // Output: // Depth 0: User // Own properties: [ 'name' ] // Depth 1: User // Own properties: [ 'constructor', 'greet' ] // Depth 2: Object // Own properties: [ 'constructor', '__defineGetter__', ... ] // Depth 3: null // Own properties: [] // End of chain
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.
console.log(Object.getPrototypeOf([]) === Array.prototype); // true console.log(Object.getPrototypeOf({}) === Object.prototype); // true console.log(Object.getPrototypeOf(/foo/) === RegExp.prototype); // true console.log(Object.getPrototypeOf(42) === Number.prototype); // true (boxed) console.log(Object.getPrototypeOf('hello') === String.prototype); // true (boxed) // null has no prototype console.log(Object.getPrototypeOf(null)); // TypeError
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.
const arr = [1, 2, 3]; console.log(Object.getPrototypeOf(arr) === Array.prototype); // true console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype) === null); // true function f() {} console.log(typeof Function.prototype === 'function'); // true console.log(Object.getPrototypeOf(f) === Function.prototype); // true console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // true const d = new Date(); console.log(Object.getPrototypeOf(d) === Date.prototype); // true console.log(Object.getPrototypeOf(Date.prototype) === Object.prototype); // true // Walk the chain let proto = arr; while (proto) { console.log(proto.constructor.name || 'null'); proto = Object.getPrototypeOf(proto); } // Output: Array, Object, null
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.
// --- Demonstrating __proto__ vs .prototype --- function Parent() {} Parent.prototype.sayHi = function() { return 'Hi from Parent'; }; function Child() {} // Correct inheritance setup: Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; Child.prototype.sayBye = function() { return 'Bye from Child'; }; const c = new Child(); // __proto__ is the instance's actual chain link console.log(c.__proto__ === Child.prototype); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true console.log(Parent.prototype.__proto__ === Object.prototype); // true // .prototype is a property on constructor functions console.log(typeof Parent.prototype); // 'object' (not undefined) console.log(typeof Child.prototype); // 'object' // Key difference: // c.__proto__ gives us the prototype object of c // Child.prototype is the same object that c's [[Prototype]] points to console.log(c.__proto__ === Child.prototype); // true, but these are different concepts // What happens if you accidentally assign to __proto__ instead of .prototype? function Misconfigured() {} const m1 = new Misconfigured(); // This does NOT change future instances — it only modifies m1's link m1.__proto__ = { hacked: true }; console.log(m1.hacked); // true const m2 = new Misconfigured(); console.log(m2.hacked); // false — m2's prototype is still Misconfigured.prototype // vs. setting .prototype: Misconfigured.prototype = { correctWay: true }; const m3 = new Misconfigured(); console.log(m3.correctWay); // true — all future instances get it // Conclusion: __proto__ is per-instance, .prototype sets the template for new instances
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.
// --- Prototype-based inheritance using constructor functions --- // PARENT constructor function Animal(name, sound) { // These become OWN properties on each instance this.name = name; this.sound = sound; } // Shared method — added to Animal.prototype so ALL instances share one copy Animal.prototype.speak = function() { return `${this.name} says ${this.sound}!`; }; Animal.prototype.describe = function() { return `${this.name} is an animal.`; }; // CHILD constructor function Dog(name) { // Step 1: Call the parent constructor to set up own properties // Without this, 'name' and 'sound' would never be set on the Dog instance Animal.call(this, name, 'Woof'); this.tricks = []; // Dog-specific own property } // Step 2: Wire up the prototype chain // Dog.prototype must have Animal.prototype in ITS chain // Object.create() creates a new object whose [[Prototype]] is Animal.prototype Dog.prototype = Object.create(Animal.prototype); // Step 3: Fix the constructor reference (Object.create broke it) // Without this, dog.constructor would point to Animal — wrong! Dog.prototype.constructor = Dog; // Dog-specific method — only Dogs have this, not Animals Dog.prototype.learnTrick = function(trick) { this.tricks.push(trick); return `${this.name} learnt: ${trick}`; }; // --- Usage --- const myDog = new Dog('Rex'); console.log(myDog.speak()); // Found on Animal.prototype via the chain console.log(myDog.describe()); // Also from Animal.prototype console.log(myDog.learnTrick('sit')); // Found on Dog.prototype console.log(myDog.tricks); // Own property on the instance // Verifying the chain console.log(myDog instanceof Dog); // true console.log(myDog instanceof Animal); // true — chain reaches Animal.prototype console.log(myDog.constructor === Dog); // true — because we fixed it in Step 3 // What the chain looks like: // myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null
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.
// --- ES6 class inheritance — same prototype chain, readable syntax --- class BankAccount { // Private field — NOT on the prototype, not accessible outside this class #balance; constructor(owner, initialDeposit) { this.owner = owner; // own property this.#balance = initialDeposit; // private own property } deposit(amount) { if (amount <= 0) throw new Error('Deposit must be positive'); this.#balance += amount; return this; // enables method chaining } getBalance() { return this.#balance; } toString() { return `${this.owner}'s account: $${this.#balance}`; } } // SavingsAccount EXTENDS BankAccount // Under the hood: SavingsAccount.prototype = Object.create(BankAccount.prototype) class SavingsAccount extends BankAccount { #interestRate; constructor(owner, initialDeposit, annualInterestRate) { // super() MUST be called before accessing 'this' in a derived class // It runs BankAccount's constructor on the new instance super(owner, initialDeposit); this.#interestRate = annualInterestRate; } applyInterest() { // getBalance() is inherited from BankAccount.prototype via the chain const interest = this.getBalance() * this.#interestRate; this.deposit(interest); // deposit() also inherited return `Interest of $${interest.toFixed(2)} applied`; } // Override toString — but call the parent version inside it toString() { // super.toString() walks up to BankAccount.prototype.toString return `${super.toString()} [Savings @ ${this.#interestRate * 100}% interest]`; } } // --- Usage --- const currentAccount = new BankAccount('Alice', 1000); currentAccount.deposit(500); // method chaining is possible because deposit returns this console.log(currentAccount.getBalance()); // 1500 console.log(currentAccount.toString()); const savingsAccount = new SavingsAccount('Bob', 2000, 0.05); console.log(savingsAccount.applyInterest()); console.log(savingsAccount.toString()); // instanceof still works — class inheritance IS prototype inheritance console.log(savingsAccount instanceof SavingsAccount); // true console.log(savingsAccount instanceof BankAccount); // true // The prototype chain is identical to the manual version: // savingsAccount --> SavingsAccount.prototype --> BankAccount.prototype --> Object.prototype --> null console.log( Object.getPrototypeOf(SavingsAccount.prototype) === BankAccount.prototype ); // true — confirms extends wired this up
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:
const proto = { login() { console.log(`${this.name} logged in`); } }; // Way 1: Object literal (default prototype) const user1 = { name: 'alice' }; console.log(Object.getPrototypeOf(user1) === Object.prototype); // true // Way 2: Constructor function function User(name) { if (!(this instanceof User)) return new User(name); this.name = name; } User.prototype = proto; const user2 = new User('bob'); user2.login(); // bob logged in // Way 3: Object.create const user3 = Object.create(proto); user3.name = 'charlie'; user3.login(); // charlie logged in // Way 4: class class UserClass { constructor(name) { this.name = name; } } Object.assign(UserClass.prototype, proto); const user4 = new UserClass('dave'); user4.login(); // dave logged in // Way 5: Object.setPrototypeOf const user5 = { name: 'eve' }; Object.setPrototypeOf(user5, proto); user5.login(); // eve logged in
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 greet(). In strict mode, 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 = () => child.greet(). Or in constructors, use the old 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.
'use strict'; const parent = { name: 'Parent', greet() { console.log(`Hello from ${this.name}`); }, sayHello() { return 'Hello ' + this.name; }, sayGoodbye() { console.log(this.sayHello() + ' and goodbye!'); } }; const child = Object.create(parent); child.name = 'Child'; child.greet(); // 'Hello from Child' child.sayGoodbye(); // 'Hello Child and goodbye!' const extracted = child.greet; // extracted(); // TypeError: Cannot read properties of undefined (reading 'name') in strict mode // Fix via bind const boundGreet = child.greet.bind(child); boundGreet(); // 'Hello from Child'
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.
'use strict'; const proto = {}; Object.defineProperty(proto, 'x', { value: 10, writable: false }); Object.defineProperty(proto, 'y', { set(val) { console.log(`Setter called with ${val}`); this._y = val * 2; }, get() { return this._y; } }); const obj = Object.create(proto); obj.x = 20; // silent failure in sloppy, TypeError in strict console.log(obj.x); // 10 (unchanged) obj.y = 5; // Setter runs — 'Setter called with 5' console.log(obj.y); // 10 (5 * 2) console.log(obj.hasOwnProperty('y')); // false — setter, not own property
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:
const userProto = { login() {} }; const user = Object.create(userProto); user.name = 'alice'; user.role = 'admin'; for (const key in user) { console.log(key); } // Output: name, role, login // Using Object.keys: Object.keys(user).forEach(key => console.log(key)); // Output: name, role // Using hasOwnProperty inside for..in: for (const key in user) { if (Object.hasOwn(user, key)) { console.log(key); } } // Output: name, role
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.
// DON'T Array.prototype.customMethod = function() { return 'patched'; }; const arr = [1, 2, 3]; for (let key in arr) { console.log(key); // 0, 1, 2, 'customMethod' } // DO function customMethod(arr) { return 'standalone'; } console.log(customMethod(arr)); // 'standalone' // Or subclass class MyArray extends Array { customMethod() { return 'subclass'; } } const myArr = new MyArray(1, 2, 3); console.log(myArr.customMethod()); // 'subclass'
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:
// Slow: deep chain class A {} class B extends A {} class C extends B {} class D extends C {} class E extends D {} class F extends E {} const obj = new F(); const start = performance.now(); for (let i = 0; i < 1000000; i++) { // accessing a property that doesn't exist on F, forces full chain walk obj.nonExistent; } console.log(`Deep chain: ${performance.now() - start}ms`); // Fast: shallow chain class Simple { constructor() { this.nonExistent = undefined; } } const simple = new Simple(); const start2 = performance.now(); for (let i = 0; i < 1000000; i++) { simple.nonExistent; } console.log(`Shallow: ${performance.now() - start2}ms`);
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.
// --- Both produce identical prototype chains --- // Prototypal way function AnimalProto(name) { this.name = name; } AnimalProto.prototype.speak = function() { return `${this.name} says...`; }; // Class way class AnimalClass { constructor(name) { this.name = name; } speak() { return `${this.name} says...`; } } // Both build the same structure const p = new AnimalProto('Proto'); const c = new AnimalClass('Class'); console.log(p.__proto__ === AnimalProto.prototype); // true console.log(c.__proto__ === AnimalClass.prototype); // true console.log(AnimalProto.prototype.constructor === AnimalProto); // true console.log(AnimalClass.prototype.constructor === AnimalClass); // true // But classes have strict mode and super requirements class Child extends AnimalClass { constructor(name, age) { super(name); // must be called before this this.age = age; } } // Same prototype chain manually: function ChildProto(name, age) { AnimalProto.call(this, name); this.age = age; } ChildProto.prototype = Object.create(AnimalProto.prototype); ChildProto.prototype.constructor = ChildProto; // The prototype chains are structurally identical
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.
// --- Mixins: composing behaviour without deep inheritance --- // Mixin factory: a function that takes a Base class and returns an extended class // This lets us 'snap in' abilities without a fixed hierarchy const Serialisable = (BaseClass) => class extends BaseClass { toJSON() { // Serialise all own enumerable properties to a JSON string const ownProps = {}; for (const key of Object.keys(this)) { ownProps[key] = this[key]; } return JSON.stringify(ownProps); } static fromJSON(json) { const data = JSON.parse(json); // Creates an instance and copies all properties on to it return Object.assign(new this(), data); } }; const Timestamped = (BaseClass) => class extends BaseClass { constructor(...args) { super(...args); // Automatically stamp with created time when constructed this.createdAt = new Date().toISOString(); } getAge() { const ms = Date.now() - new Date(this.createdAt).getTime(); return `${Math.floor(ms / 1000)} seconds old`; } }; // The base class stays lean — only core identity class User { constructor(username, email) { this.username = username; this.email = email; } greet() { return `Hello, I'm ${this.username}`; } } // Compose: User + Timestamped + Serialisable // Read right to left: start with User, add Timestamped, add Serialisable class PlatformUser extends Serialisable(Timestamped(User)) { constructor(username, email, role) { super(username, email); // chains up through all the mixins to User this.role = role; } } // --- Usage --- const admin = new PlatformUser('sarah_dev', 'sarah@example.com', 'admin'); console.log(admin.greet()); // from User console.log(admin.getAge()); // from Timestamped mixin console.log(admin.toJSON()); // from Serialisable mixin // instanceof still works for all layers console.log(admin instanceof User); // true console.log(admin instanceof PlatformUser); // true // The prototype chain records every mixin link: // admin // --> PlatformUser.prototype // --> Serialisable(Timestamped(User)).prototype [anonymous class] // --> Timestamped(User).prototype [anonymous class] // --> User.prototype // --> Object.prototype // --> null
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.
// --- Example of Prototype Pollution and Mitigation --- // UNSAFE merge — vulnerable to pollution function unsafeMerge(target, source) { for (const key in source) { if (typeof source[key] === 'object' && source[key] !== null) { target[key] = unsafeMerge(target[key] || {}, source[key]); } else { target[key] = source[key]; } } return target; } // Attacker sends payload with __proto__ const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}'); const innocentConfig = { role: 'user' }; unsafeMerge(innocentConfig, maliciousPayload); // Now every object in the app has isAdmin: true console.log(({}).isAdmin); // true — pollution occurred! // --- Safe alternatives --- // 1. Use Object.create(null) for maps const safeConfig = Object.create(null); safeConfig.role = 'user'; // merging into safeConfig won't affect global Object.prototype // 2. Check for __proto__ and constructor in merge function safeMerge(target, source) { for (const key in source) { if (key === '__proto__' || key === 'constructor') continue; if (typeof source[key] === 'object' && source[key] !== null) { target[key] = safeMerge(target[key] || {}, source[key]); } else { target[key] = source[key]; } } return target; } // 3. Freeze Object.prototype in critical apps // Object.freeze(Object.prototype); // uncomment with caution console.log(safeMerge({}, { __proto__: { isAdmin: true } }).__proto__); // undefined
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.
// io.thecodeforge — javascript tutorial const vehicle = { type: 'car', wheels: 4 }; const sedan = Object.create(vehicle); sedan.doors = 4; console.log(sedan.wheels); // 4 (inherited) console.log(sedan.hasOwnProperty('wheels')); // false sedan.wheels = 6; // shadowing — does NOT touch vehicle console.log(vehicle.wheels); // 4 (unchanged) console.log(sedan.hasOwnProperty('wheels')); // true console.log(sedan.wheels); // 6 (own property now)
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.
// io.thecodeforge — javascript tutorial const level1 = { a: 1 }; const level2 = Object.create(level1); const level3 = Object.create(level2); const level4 = Object.create(level3); const level5 = Object.create(level4); // Property lookup walks 5 links console.time('deep-lookup'); for (let i = 0; i < 1e6; i++) { level5.a; } console.timeEnd('deep-lookup'); // Flat object, 1 lookup const flat = { a: 1 }; console.time('flat-lookup'); for (let i = 0; i < 1e6; i++) { flat.a; } console.timeEnd('flat-lookup');
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()
// io.thecodeforge — javascript tutorial const withLogging = { log(method, url) { console.log(`[${new Date().toISOString()}] ${method} ${url}`); } }; const withRetry = { async fetchWithRetry(url, options, retries = 3) { for (let attempt = 1; attempt <= retries; attempt++) { try { return await fetch(url, options); } catch (err) { if (attempt === retries) throw err; await new Promise(r => setTimeout(r, 100 * attempt)); } } } }; function createApiClient(baseUrl) { return { baseUrl, ...withLogging, ...withRetry, async get(endpoint) { this.log('GET', `${this.baseUrl}${endpoint}`); return this.fetchWithRetry(`${this.baseUrl}${endpoint}`); } }; } const client = createApiClient('https://api.example.com'); console.log(Object.getPrototypeOf(client)); // Object.prototype
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.
// io.thecodeforge — javascript tutorial const animal = { speak() { return `${this.name} makes a sound`; } }; const dog = Object.create(animal); dog.name = 'Rex'; // Inherits speak from animal console.log(dog.speak()); // True empty object — no prototype at all const safeMap = Object.create(null); safeMap['__proto__'] = 'polluted'; // Safe from pollution console.log(Object.keys(safeMap)); // ['__proto__']
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.
// io.thecodeforge — javascript tutorial const base = { greet() { return 'Hi'; } }; const obj = { name: 'Alice' }; // Don't do this in production unless you have no choice Object.setPrototypeOf(obj, base); console.log(obj.greet()); // 'Hi' // The cost: obj becomes 'slow' in V8 // Check performance in DevTools — it's real // Better: build it right from the start const betterObj = Object.assign(Object.create(base), { name: 'Alice' }); console.log(betterObj.greet()); // 'Hi'
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.
// io.thecodeforge — javascript tutorial function Animal() {} Animal.prototype.eat = function() {}; const dog = new Animal(); dog.bark = function() {}; const proto = Object.getPrototypeOf(dog); console.log(proto === Animal.prototype); // true let current = dog; const chain = []; while (current) { chain.push(current.constructor?.name || 'null'); current = Object.getPrototypeOf(current); } console.log(chain); // ['Animal', 'Object', 'null']
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.
// io.thecodeforge — javascript tutorial const animal = { eat() { return 'yum'; } }; const dog = Object.create(animal); dog.bark = () => 'woof'; console.log(dog.eat()); // yum (inherited) console.log(Object.getPrototypeOf(dog) === animal); // true const nullMap = Object.create(null); nullMap.key = 'value'; console.log(nullMap.toString); // undefined — no Object.prototype
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__ chaininstance.hasOwnProperty('methodName')Object.getOwnPropertyDescriptor(instance, 'methodName')instance.constructor === ExpectedClassObject.getPrototypeOf(instance).constructor.nameObject.keys(Object.prototype).filter(k => !['constructor','toString','valueOf','hasOwnProperty','isPrototypeOf','propertyIsEnumerable','toLocaleString'].includes(k))JSON.parse(userInput) — ensure input doesn't contain __proto__| Aspect | Constructor Functions (.prototype) | ES6 Classes (class / extends) |
|---|---|---|
| Syntax clarity | Verbose — chain wiring is manual | Clean — extends and super handle it |
| Prototype chain | Identical underlying mechanism | Identical underlying mechanism |
| Private state | Requires closure workaround | Native private fields with # prefix |
| Calling parent constructor | Animal.call(this, ...args) | super(...args) — must be first |
| Calling parent method | Animal.prototype.method.call(this) | super.method() |
| Hoisting | Function declarations are hoisted | Classes are NOT hoisted — use after declaration |
| Strict mode | Optional | Always in strict mode automatically |
| Seen in codebases | Legacy and transpiled output (Babel) | Modern JS, TypeScript, frameworks |
| File | Command / Code | Purpose |
|---|---|---|
| prototype-chain-basics.js | const vehicleBlueprint = { | What the Prototype Chain Actually Is |
| io | function walkChain(obj) { | Prototype Chain Hierarchy Visual Diagram |
| io | console.log(Object.getPrototypeOf([]) === Array.prototype); // true | Implicit Constructors of Literals |
| io | const arr = [1, 2, 3]; | Built-In Types in the Chain |
| io | function Parent() {} | The __proto__ vs prototype Distinction Guide |
| constructor-inheritance.js | function Animal(name, sound) { | Constructor Functions and .prototype |
| class-inheritance.js | class BankAccount { | ES6 Classes |
| io | const proto = { | Five Ways to Create and Mutate a Prototype Chain |
| io | 'use strict'; | this in Inherited Methods |
| io | 'use strict'; | Writing to the Prototype |
| io | const userProto = { | Iterating Own vs Inherited Properties |
| io | Array.prototype.customMethod = function() { return 'patched'; }; | Monkey Patching Built-In Prototypes |
| io | class A {} | Performance Implications of Deep Prototype Chains |
| io | function AnimalProto(name) { this.name = name; } | Class vs Prototype (Sugar vs Foundation) Comparison Table |
| mixins-composition.js | const Serialisable = (BaseClass) => class extends BaseClass { | Real-World Pattern |
| prototype-pollution.js | function unsafeMerge(target, source) { | Prototype Pollution |
| ShadowingChain.js | const vehicle = { type: 'car', wheels: 4 }; | Inheritance Is Just Object Linking |
| DeepChainPerf.js | const level1 = { a: 1 }; | Building Longer Chains Is Usually a Mistake |
| FactoryComposition.js | const withLogging = { | Factory Functions Beat Prototypes for Composable Behavior |
| ObjectCreateInheritance.js | const animal = { | Object.create() |
| SetPrototypeOfDanger.js | const base = { greet() { return 'Hi'; } }; | Object.setPrototypeOf() |
| inspect-chain.js | function Animal() {} | Inspecting Prototypes |
| object-create.js | const animal = { eat() { return 'yum'; } }; | Object.create() |
Key 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.What 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?
ES6 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?
Explain prototype pollution — what is it, how does it work, and how do you prevent it?
Frequently Asked Questions
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.
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.
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.
Use Object.getPrototypeOf() in a loop until null. Or in browser DevTools, console.dir(obj) shows the full chain. Avoid __proto__ in production code.
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).
Yes — for..in iterates all enumerable properties, including inherited ones from the prototype chain. This is a frequent source of bugs when you add methods to a shared prototype and then iterate object instances. Use Object.keys(obj) for own-only enumerable properties, or guard with if (Object.hasOwn(obj, key)) inside the loop. Object.hasOwn() is the modern replacement for the older obj.hasOwnProperty(key).
__proto__ is the actual link between an object instance and its prototype — it's the [[Prototype]] internal slot exposed as a property. prototype is a property on constructor functions that becomes the [[Prototype]] of objects created with new. When you write new Foo(), the resulting object's __proto__ is set to Foo.prototype. They're two sides of the same chain: prototype defines it, __proto__ accesses it.
Because this is determined by the call site, not by where the method is defined. When you write const fn = obj.method and then call fn(), there is no object before the dot — so this becomes undefined in strict mode (or the global object in sloppy mode). The method travels without its context. Fix it with const fn = obj.method.bind(obj) to permanently attach obj as this, or use an arrow function wrapper: const fn = () => obj.method().
In 2018, TC39 proposed Array.prototype.flatten(). It turned out MooTools (a library from 2006 still running on millions of sites) had already added Array.prototype.flatten with different semantics. Browsers couldn't implement the new flatten without breaking those sites. The proposal had to be renamed to Array.prototype.flat(). The lesson: the moment you patch a built-in prototype, you've made a global contract that is nearly impossible to revoke. Never patch built-ins — use standalone utility functions instead.
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?
18 min read · try the examples if you haven't