Here's a question most JavaScript developers can't answer precisely: when you call array.push(1), where does push come from? It's not on the array object itself. You didn't define it. Yet it works on every array, everywhere, with the correct behavior.
The answer is prototypes — the single mechanism that makes JavaScript's object system work. Classes didn't replace it. Classes are prototypes with better syntax.
Every Object Has a Prototype
Every JavaScript object (except objects explicitly created without one) has an internal [[Prototype]] slot — a reference to another object. This creates a chain. When you access a property on an object, JavaScript first looks at the object itself. If it's not there, it follows the [[Prototype]] link to the next object and looks there. It keeps following the chain until either the property is found or null is reached.
const obj = { name: 'Alice' }
// obj's [[Prototype]] is Object.prototype // Object.prototype's [[Prototype]] is null — end of the chain
obj.name // 'Alice' — found on obj directly obj.toString() // found on Object.prototype, not obj obj.missing // undefined — not found anywhere in the chain
This is the complete picture of property lookup in JavaScript. No special cases. Every property access in JavaScript follows this path.
The Actual Chain for Arrays
const arr = [1, 2, 3]
arr.push(4) // where is push? arr.map(x => x * 2) // where is map? arr.toString() // where is toString?
The chain: arr → Array.prototype → Object.prototype → null
push,pop,map,filter,reduce,forEach— all onArray.prototype
toString,hasOwnProperty,valueOf— onObject.prototype
- Own properties:
arr[0],arr[1],arr[2],arr.length— directly onarr
When you create any array, it gets Array.prototype as its prototype — automatically. You don't have to do anything. The engine sets this up for you.
This is also why you can check Array.prototype:
Array.prototype.push // the actual push function — same one every array uses
[].push === [].push // true — same function reference, just found via prototype
One push function. Shared by every array in your program.
Constructor Functions: How Objects Got Prototypes Before Classes
Before ES6 classes, constructor functions were the standard way to create objects with shared behavior:
function User(name, email) {
// These become own properties — stored on each instance
this.name = name
this.email = email
}
// Methods go on the prototype — shared by all instances User.prototype.greet = function() { return Hi, I'm ${this.name} }
User.prototype.getEmailDomain = function() { return this.email.split('@')[1] }
const alice = new User('Alice', 'alice@example.com') const bob = new User('Bob', 'bob@example.com')
alice.greet() // "Hi, I'm Alice" bob.greet() // "Hi, I'm Bob" alice.greet === bob.greet // true — same function, shared via prototype
One greet function. Stored once. Used by every User instance.
What new Actually Does
The new keyword is doing four specific things. Knowing them makes new non-magical:
function User(name) {
this.name = name
}
// What new User('Alice') does: // Step 1: Creates a new empty object const instance = {}
// Step 2: Sets its [[Prototype]] to User.prototype Object.setPrototypeOf(instance, User.prototype)
// Step 3: Calls User with 'this' bound to the new object User.call(instance, 'Alice')
// Step 4: Returns the instance (unless User explicitly returns a different object) // → instance is { name: 'Alice' }, linked to User.prototype
Implementing new manually is a classic interview question — it demonstrates you understand all four steps:
function myNew(Constructor, ...args) {
const instance = Object.create(Constructor.prototype)
const result = Constructor.apply(instance, args)
return result !== null && typeof result === 'object' ? result : instance
}
const alice = myNew(User, 'Alice') alice.greet() // "Hi, I'm Alice" — works identically
Classes: The Same Mechanism, Better Syntax
ES6 classes are syntactic sugar. Under the hood, they produce identical prototype chains to constructor functions. class is a keyword that writes the prototype wiring for you:
class User {
constructor(name, email) {
this.name = name // own property
this.email = email // own property
}
greet() { // → User.prototype.greet return Hi, I'm ${this.name} }
getEmailDomain() { // → User.prototype.getEmailDomain return this.email.split('@')[1] }
static create(name, email) { // → User.create (on the constructor, not prototype) return new User(name, email) } }
// Proof that it's the same: typeof User // 'function' — a class IS a function User.prototype.greet // the greet method is on the prototype Object.getPrototypeOf(new User()) // User.prototype
Inheritance with classes:
class Admin extends User {
constructor(name, email, permissions) {
super(name, email) // calls User's constructor — MUST come first
this.permissions = permissions // own property
}
can(action) { // → Admin.prototype.can return this.permissions.includes(action) } }
const admin = new Admin('Charlie', 'charlie@corp.com', ['read', 'write'])
// Prototype chain: admin → Admin.prototype → User.prototype → Object.prototype → null admin.greet() // found on User.prototype via chain admin.can('read') // found on Admin.prototype admin instanceof Admin // true admin instanceof User // true — User.prototype is in the chain
extends sets up Admin.prototype's prototype to be User.prototype. That's all inheritance is — linking prototype chains.
Prototype vs Own Properties: The Distinction That Matters
const alice = new User('Alice', 'alice@example.com')
// Own properties — stored directly on alice alice.hasOwnProperty('name') // true alice.hasOwnProperty('email') // true
// Prototype properties — accessed via chain, not stored on alice alice.hasOwnProperty('greet') // false — greet is on User.prototype
// The 'in' operator checks the entire chain: 'name' in alice // true — own property 'greet' in alice // true — found on prototype 'push' in alice // false — not in this chain
// Object.keys — own enumerable only: Object.keys(alice) // ['name', 'email'] — prototype methods not listed
Why this matters: for...in iterates all enumerable properties, including inherited ones. If you've added anything to Object.prototype (a bad idea), it shows up in every for...in loop everywhere. Always use Object.keys() or Object.hasOwn(obj, key) to stay safe.
Object.create: Prototypal Inheritance Without Constructors
Object.create(proto) creates a new object with proto as its [[Prototype]] — no constructor needed:
const vehicle = {
type: 'vehicle',
describe() {
return I am a ${this.type} named ${this.name}
}
}
const car = Object.create(vehicle) car.type = 'car' car.name = 'Tesla'
car.describe() // 'I am a car named Tesla' — method from vehicle, data from car
// Object.create(null) — no prototype at all const dict = Object.create(null) dict.toString // undefined — genuinely no inherited properties // Safe as a key-value store when keys could conflict with Object.prototype methods
Prototype Pollution: Why This Knowledge Is a Security Concern
Since all plain objects share Object.prototype, modifying it affects everything:
// Never do this — but understand why libraries prevent it:
Object.prototype.isAdmin = true
const user = {} user.isAdmin // true — even an empty object now has this!
// This is prototype pollution — a real vulnerability class // Attack: user-controlled input like: // JSON.parse('{"__proto__": {"isAdmin": true}}') // when merged naively into an object, can inject properties onto Object.prototype
// Safe merge: const merged = Object.assign(Object.create(null), userInput) // or const merged = { ...userInput } // spread doesn't affect __proto__
The Interview Questions Prototypes Actually Appear In
"What is the difference between prototypal and classical inheritance?" Classical (C++, Java): objects are instances of classes; inheritance is a copy mechanism. Prototypal (JavaScript): objects inherit from other objects directly; properties are delegated via the live chain, not copied. Classes in JavaScript are syntax for prototypal inheritance, not a fundamentally different system.
"What does instanceof do?" Checks if Constructor.prototype exists anywhere in the object's prototype chain. Not whether the object was constructed by that constructor specifically — just whether the prototype appears in the chain. alice instanceof User is true if User.prototype is alice's prototype or any ancestor's prototype.
"How do you implement inheritance without classes?" Object.create(): const Child = Object.create(Parent). Constructor functions + Child.prototype = Object.create(Parent.prototype) + Child.prototype.constructor = Child. These produce identical chains to ES6 class inheritance.
"What's the difference between __proto__ and prototype?" prototype is a property on functions — it becomes the [[Prototype]] of objects created with new ThatFunction(). __proto__ is an accessor on objects that lets you read/write the [[Prototype]] slot. __proto__ is deprecated — use Object.getPrototypeOf() and Object.setPrototypeOf() instead.