Prototypes & Inheritance in JavaScript
Every object in JavaScript has a hidden link to another object. That hidden link points to its prototype. Think of it as a behind-the-scenes connection that JavaScript uses automatically.
When you try to use a property or method on an object, JavaScript first looks at the object itself. If it doesn't find it there, it follows that hidden link and checks the prototype. If it still can't find it, it goes up another level. This is called the prototype chain.
Here's a simple real-world way to think about it. Imagine you don't know the answer to a question. You ask your parent. They don't know either, so they ask their parent. That chain of asking — up through generations — is exactly how the prototype chain works in JavaScript.
You can see the prototype of an object using Object.getPrototypeOf(), or the (unofficial but widely supported) __proto__ property.
const animal = {
speak() {
console.log("Some generic sound...");
}
};
const dog = Object.create(animal); // dog's prototype is animal
dog.name = "Rex";
dog.speak(); // "Some generic sound..." — found on the prototype
console.log(dog.name); // "Rex" — found on dog itself
// Check the prototype link
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(dog.__proto__ === animal); // true (same thing, older syntax)
In the example above, speak() is not on the dog object directly. But because dog's prototype is animal, JavaScript finds it there. That's the prototype chain in action.
The prototype chain is a series of linked objects. It goes: your object → its prototype → that prototype's prototype → and so on, all the way up to null. When JavaScript hits null, it stops looking and returns undefined.
A great analogy is a family tree. You might inherit your eye colour from your parent, who inherited their height from their grandparent. Each generation passes traits down. In JavaScript, each object passes methods and properties down through the chain.
This is why things like .toString() and .hasOwnProperty() work on almost every object in JavaScript — they live way up at the top of the chain, on Object.prototype.
const obj = { name: "Alice" };
// hasOwnProperty lives on Object.prototype, not on obj directly
console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("age")); // false
// toString also lives on Object.prototype
console.log(obj.toString()); // "[object Object]"
// Let's trace the chain manually
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null — end of chain
// A deeper chain example
function Vehicle() {}
function Car() {}
Car.prototype = Object.create(Vehicle.prototype);
const myCar = new Car();
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Vehicle); // true — it's in the chain!
console.log(Object.getPrototypeOf(Object.getPrototypeOf(myCar)) === Vehicle.prototype); // true
Every object you create in JavaScript ultimately has Object.prototype somewhere at the top of its chain. That is where all those built-in helpers come from.
Object.create() is one of the cleanest ways to set up inheritance in JavaScript. It lets you create a brand new object and explicitly set its prototype in one step.
Think of it like this: imagine a new employee joins a company. Instead of handing them every single company rule personally, you just give them a link to the company handbook. Object.create() is that handshake — the new object inherits everything from the handbook (its prototype) automatically.
You can also pass a second argument to Object.create() with property descriptors to define properties directly on the new object at the same time.
const personProto = {
greet() {
console.log(`Hi, I'm ${this.name}!`);
},
introduce() {
console.log(`I work as a ${this.job}.`);
}
};
// Create a new object with personProto as its prototype
const alice = Object.create(personProto);
alice.name = "Alice";
alice.job = "developer";
alice.greet(); // "Hi, I'm Alice!"
alice.introduce(); // "I work as a developer."
// alice itself only has name and job
console.log(Object.keys(alice)); // ["name", "job"]
// But it can still use greet and introduce via the prototype
console.log(Object.getPrototypeOf(alice) === personProto); // true
// Using the second argument to set own properties at creation time
const bob = Object.create(personProto, {
name: { value: "Bob", writable: true, enumerable: true },
job: { value: "designer", writable: true, enumerable: true }
});
bob.greet(); // "Hi, I'm Bob!"
Object.create(null) creates an object with no prototype at all — useful when you want a truly clean dictionary-style object with zero inherited behaviour.
ES6 introduced the class keyword, and it was a big deal for readability. But here's the important thing to understand: classes in JavaScript do not change how the language works. Under the hood, it is still prototypes all the way down.
A class is like a nice dashboard on your car. The dashboard makes things easier to use — buttons, dials, a clean layout. But behind the dashboard, the engine still works exactly the same way. JavaScript classes are syntactic sugar on top of prototype-based inheritance.
When you define a method inside a class, JavaScript puts it on the class's prototype automatically. You can verify this yourself — and it's a great thing to know for interviews.
// ---- Using a class (ES6 syntax) ----
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
const cat = new Animal("Whiskers");
cat.speak(); // "Whiskers makes a noise."
// ---- Doing the exact same thing with prototypes directly ----
function AnimalProto(name) {
this.name = name;
}
AnimalProto.prototype.speak = function () {
console.log(`${this.name} makes a noise.`);
};
const cat2 = new AnimalProto("Whiskers");
cat2.speak(); // "Whiskers makes a noise."
// ---- Proof that class methods live on the prototype ----
console.log(typeof Animal); // "function" — classes are functions!
console.log(cat.speak === Animal.prototype.speak); // true
Both approaches produce the same result. The class syntax is much easier to read and write, especially as your code grows. But knowing the prototype mechanics behind it will make you a stronger developer — and will definitely come up in interviews.
Have you ever wondered why every array can call .push(), or every string can call .toUpperCase()? It is because arrays and strings each have a built-in prototype packed full of shared methods.
Think of all dogs sharing the same species blueprint. Every dog, no matter who owns it, can bark. The blueprint is Dog.prototype (so to speak). In JavaScript, every array shares Array.prototype and every string shares String.prototype. That's where all those helpful methods live.
You can even add your own methods to these built-in prototypes — though it is generally not recommended in production code, as it can break other code that shares the same prototype. It is fine in small experiments or learning exercises though.
const nums = [1, 2, 3];
// .push() lives on Array.prototype — not on nums directly
console.log(nums.hasOwnProperty("push")); // false
console.log("push" in nums); // true (found on prototype)
console.log(Array.prototype.hasOwnProperty("push")); // true
// Same idea with strings
const greeting = "hello";
console.log(String.prototype.hasOwnProperty("toUpperCase")); // true
console.log(greeting.toUpperCase()); // "HELLO"
// The chain for an array goes:
// nums → Array.prototype → Object.prototype → null
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
// Adding a custom method to Array.prototype (for learning only!)
Array.prototype.sum = function () {
return this.reduce((acc, val) => acc + val, 0);
};
console.log([1, 2, 3, 4].sum()); // 10
This is also why typeof [] returns "object" — arrays are objects, just with Array.prototype in their chain giving them all those extra powers.
Here are the key things to take away from this tutorial:
- Every object in JavaScript has a hidden link to a prototype object.
- When JavaScript looks for a property, it walks up the prototype chain until it either finds it or reaches null.
- Object.create(proto) lets you create a new object with a specific prototype, giving full control over inheritance.
- ES6 class syntax is just a cleaner way to write prototype-based code — it does not change how JavaScript actually works under the hood.
- Built-in types like Array, String, and Object all have their own prototypes (Array.prototype, String.prototype, Object.prototype) where shared methods live.
- All prototype chains in JavaScript eventually end at Object.prototype, and then null.
- You can check if a property belongs directly to an object (not inherited) using hasOwnProperty().
- Avoid modifying built-in prototypes in production code — it can cause unexpected behaviour across your entire codebase.
- What is a prototype in JavaScript? — A prototype is an object that other objects inherit properties and methods from. Every JavaScript object has an internal link ([[Prototype]]) pointing to its prototype.
- What is the prototype chain? — The prototype chain is the series of linked objects JavaScript traverses when looking up a property. It ends when it reaches null.
- What is the difference between __proto__ and prototype? — __proto__ is the actual link on an instance pointing to its prototype. prototype is a property on constructor functions (and classes) that becomes the __proto__ of instances created with new.
- What does Object.create() do? — It creates a new object and sets the given argument as its prototype. Object.create(null) creates an object with no prototype at all.
- Are JavaScript classes just syntactic sugar? — Yes. The class keyword introduced in ES6 makes prototype-based inheritance easier to write, but it doesn't change the underlying prototype mechanism. typeof MyClass returns "function".
- How can you check if a property is directly on an object (not inherited)? — Use obj.hasOwnProperty("prop"). This returns true only if the property exists on the object itself, not on its prototype chain.
- Where do methods like .push() and .map() come from on arrays? — They live on Array.prototype. Every array instance inherits from Array.prototype through the prototype chain.
- What happens at the end of the prototype chain? — The last prototype in any chain is Object.prototype. Its own prototype is null, which signals the end of the chain. If a property is not found by then, undefined is returned.
- Is it safe to modify built-in prototypes like Array.prototype? — Generally no, especially in shared or production codebases. It can cause naming conflicts and unpredictable behaviour in third-party libraries. It is sometimes done in polyfills, but carefully.
- What is prototypal inheritance vs classical inheritance? — Classical inheritance (like in Java or C++) uses classes as strict blueprints. Prototypal inheritance (like in JavaScript) links objects directly to other objects. JavaScript's class syntax mimics classical style but is still prototypal underneath.