JavaScript’s inheritance model is simpler than its syntax makes it look. Nothing is copied from a class blueprint into your objects. There are only objects, and each one can point to another object to consult when it doesn’t have a property itself. Everything else, including constructor functions, the new operator, the class keyword and instanceof, is machinery for setting up or inspecting those links. Once you can draw the chain for a given object, prototype questions stop being tricky.
Examples run as ES modules; outputs are from Node 22.
The prototype chain
Every object has an internal slot, [[Prototype]], holding another object or null. To read a property, the engine checks the object’s own properties, then follows [[Prototype]] and checks that object, and so on until it finds the property or reaches null (result: undefined).
const animal = {
eats: true,
walk() {
return `${this.name} walks`;
},
};
const rabbit = Object.create(animal); // rabbit.[[Prototype]] = animal
rabbit.name = 'Rabbit';
console.log(rabbit.walk()); // → Rabbit walks (found on animal, `this` is rabbit)
console.log(rabbit.eats); // → true
console.log(rabbit.flies); // → undefined (searched the whole chain)
console.log(Object.getPrototypeOf(rabbit) === animal); // → true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // → true
console.log(Object.getPrototypeOf(Object.prototype)); // → null (end of the chain)The chain here is rabbit → animal → Object.prototype → null. Notice that walk was found on animal but ran with this set to rabbit. The chain decides where a method is found; the call site still decides this (see the this keyword).
Shadowing and writes
An own property with the same name hides, or shadows, the inherited one; delete the own property and the inherited one shows through again. That matters because reads walk the chain but plain assignments do not. Assigning creates or updates an own property, even when the old value came from the prototype:
const proto = { count: 0 };
const a = Object.create(proto);
a.count += 1; // reads 0 through the chain, writes 1 as an *own* property of a
console.log(a.count); // → 1
console.log(proto.count); // → 0 (untouched)
console.log(Object.hasOwn(a, 'count')); // → trueTwo exceptions: if the chain has a setter for that name, the setter runs instead, and if it has a non-writable property with that name, the assignment fails (a TypeError in strict mode, silently in sloppy mode).
Gotcha
The write rule doesn’t cover mutation. With
Team.prototype.members = [], callingfrontend.members.push('Ada')reads the shared array through the chain and mutates it, sobackend.memberscontains'Ada'too. Keep instance data in the constructor, methods on the prototype.
__proto__ vs prototype
Similar names, different things:
[[Prototype]]is the internal link every object has. Read it withObject.getPrototypeOf(obj); set it at creation withObject.create(proto).obj.__proto__is a legacy accessor for the same link. It lives onObject.prototype, so it only works on objects that inherit from there.prototypeis an ordinary property of regular functions and classes: the object that becomes the[[Prototype]]of instances created withnew. Instances don’t have one, and neither do arrow functions.
function Dog(name) {
this.name = name;
}
const rex = new Dog('Rex');
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // → true
console.log(rex.__proto__ === Dog.prototype); // → true (legacy accessor)
console.log(Dog.prototype.constructor === Dog); // → true
console.log(rex.prototype); // → undefined (instances don't get one)
console.log(Object.getPrototypeOf(Dog) === Function.prototype); // → trueObject.create(null) makes an object with no prototype at all (no toString, not even __proto__), which is a safe dictionary for arbitrary keys.
Interview tip
One sentence covers it: “
prototypeis a property on a constructor;[[Prototype]], exposed as__proto__, is the link on every object;newsets the second to the first.”
Object.create and new, step by step
Object.create(proto) is the most direct tool: it makes an empty object whose [[Prototype]] is proto. The new operator does the same linking as part of a bigger job. new Constructor(...args):
- Creates a new empty object.
- Sets its
[[Prototype]]toConstructor.prototype(or toObject.prototypeif that isn’t an object). - Calls
Constructorwiththisbound to the new object. - Returns the new object, unless the constructor explicitly returned a different object.
Interviewers often ask you to implement it:
function myNew(Constructor, ...args) {
// 1 + 2. Create an object linked to Constructor.prototype
const proto = Constructor.prototype;
const obj = Object.create(Object(proto) === proto ? proto : Object.prototype);
// 3. Run the constructor with `this` = the new object
const result = Constructor.apply(obj, args);
// 4. An object returned explicitly wins; primitives are ignored
return Object(result) === result ? result : obj;
}
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const ada = myNew(Person, 'Ada');
console.log(ada.greet()); // → Hi, I'm Ada
console.log(ada instanceof Person); // → trueObject(x) === x is a compact “is this an object?” check. Step 4 is the one people forget: a constructor ending in return { b: 2 } makes new produce { b: 2 }, while return 42 is ignored and you get the new object. myNew handles constructor functions only: class constructors throw without new, so for classes you would reach for Reflect.construct.
class is mostly sugar
A class declaration builds the same structure: a constructor function, a prototype object holding the methods, and prototype links for extends.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
static create(name) {
return new this(name);
}
}
class Dog extends Animal {
speak() {
return `${super.speak()}, then barks`;
}
}
console.log(typeof Animal); // → function
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // → true
console.log(Object.getPrototypeOf(Dog) === Animal); // → true (statics are inherited)
console.log(Dog.create('Rex').speak()); // → Rex makes a sound, then barksextends sets up two links: Dog.prototype → Animal.prototype for instance methods and Dog → Animal for statics. The “mostly” covers real differences:
- Calling a class without
newthrows:Class constructor Animal cannot be invoked without 'new'. - Class methods are non-enumerable:
Object.keys(Animal.prototype)is[]. - Class bodies are always strict, and class declarations sit in the TDZ until their line runs (see hoisting and the TDZ).
- A derived constructor must call
super()before touchingthis, because the base constructor creates the object. - Private
#fieldsare not properties, so they never appear on the chain.
Checking the chain
instanceof
value instanceof Constructor asks one question: does Constructor.prototype appear anywhere in value’s prototype chain? It says nothing about which function actually created the object.
function myInstanceOf(value, Constructor) {
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
return false; // primitives are never instances
}
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(myInstanceOf(rex, Animal), rex instanceof Animal); // → true true
console.log(myInstanceOf(rex, Array), rex instanceof Array); // → false false
console.log(myInstanceOf(5, Number), 5 instanceof Number); // → false falseThat definition explains the edge cases. Replace Cat.prototype after creating tom, and tom instanceof Cat becomes false. An array from another iframe fails instanceof Array because it has a different Array.prototype, hence Array.isArray. Classes can customize the check with Symbol.hasInstance.
Own vs inherited properties
const user = Object.create({ role: 'admin' });
user.name = 'Ada';
console.log('role' in user); // → true (checks the whole chain)
console.log(Object.hasOwn(user, 'role')); // → false (own properties only)
console.log(Object.hasOwn(user, 'name')); // → true
const dict = Object.create(null);
dict.key = 1;
console.log(Object.hasOwn(dict, 'key')); // → true
try {
dict.hasOwnProperty('key');
} catch (e) {
console.log(e.message); // → dict.hasOwnProperty is not a function
}hasOwnProperty is itself inherited, so it is missing on null-prototype objects and can be shadowed by an own property. The static Object.hasOwn (ES2022) avoids both problems. Similarly, for...in visits inherited enumerable properties; Object.keys returns own ones only.
Performance notes
- Engines cache lookups by object shape, so ordinary chain depth costs little. Stable shapes matter more than chain length.
- Changing an existing object’s prototype (
Object.setPrototypeOf,obj.__proto__ = ...) is slow in every major engine and can slow down later code using that object. Create objects with the right prototype up front. - Prototype methods are shared: a thousand instances use one
speak. Per-instance functions (assigned in the constructor, or arrow class fields) cost one allocation each. - Don’t extend built-in prototypes. A library once added
Array.prototype.flatten, and the standard method had to ship asflatto avoid breaking sites.
Inheritance without class
A common prompt: “Make Dog inherit from Animal without using class.” There are four steps:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound`;
};
function Dog(name, breed) {
Animal.call(this, name); // 1. run the parent constructor on this object
this.breed = breed;
}
// 2. Link the prototypes: Dog.prototype → Animal.prototype
Dog.prototype = Object.create(Animal.prototype, {
// 3. Restore constructor (non-enumerable, like the original)
constructor: { value: Dog, writable: true, configurable: true },
});
// 4. Override and call the parent method ("super")
Dog.prototype.speak = function () {
return `${Animal.prototype.speak.call(this)} and barks`;
};const rex = new Dog('Rex', 'Labrador');
console.log(rex.speak()); // → Rex makes a sound and barks
console.log(rex instanceof Dog, rex instanceof Animal); // → true true
console.log(rex.constructor === Dog); // → true
console.log(Object.keys(rex)); // → [ 'name', 'breed' ]
console.log(Object.keys(Dog.prototype)); // → [ 'speak' ]
Object.setPrototypeOf(Dog, Animal); // optional: inherit statics, like `extends`The last line is one-time setup on a fresh function, so the performance note above doesn’t apply. Two tempting shortcuts are wrong. Dog.prototype = Animal.prototype makes both constructors share one object, so defining Dog.prototype.speak makes every Animal bark. Dog.prototype = new Animal() runs the parent constructor at setup time, without arguments, and leaves a stray name property on the prototype.
Note
Object.keys(Dog.prototype)listsspeakbecause it was assigned normally; aclassmethod would be non-enumerable. Define it withObject.definePropertyif you need an exact match.
The interview answer
“Every object has an internal [[Prototype]] link to another object or null. Reading a property the object doesn’t have follows that chain until it’s found or the chain ends; assignments create own properties instead. A constructor’s prototype property is the object new links to: new creates an object, sets its [[Prototype]] to Constructor.prototype, runs the constructor with this as that object, and returns it. That’s how instances share methods.
class is mostly syntax over the same mechanism, with stricter rules: it requires new, its methods are non-enumerable, and extends also links the constructors for statics. Without class, I’d call Parent.call(this) in the child constructor, set Child.prototype = Object.create(Parent.prototype), and restore constructor. To inspect the chain I use instanceof and Object.hasOwn.”