JavaScript Prototypes – Understanding Prototypal Inheritance
Yesterday, I learned about Object-Oriented Programming in JavaScript, including classes, constructors, methods, inheritance, and the this keyword.
Today, I went deeper into how JavaScript actually implements inheritance.
The key concept is Prototypes.
JavaScript isn't fundamentally a class-based language like Java or C++. It uses a prototype-based object model.
Modern class syntax makes object-oriented programming easier to write, but underneath the surface, JavaScript still relies on prototypes.
Let's dive in.
What is a Prototype?
Every JavaScript object has an internal link to another object called its prototype.
That prototype can provide properties and methods that the object can access.
For example:
const user = {
name: "Saurabh"
};
console.log(user.toString());
We didn't define toString() inside user.
So where did it come from?
JavaScript looks up the object's prototype and finds the method there.
This is called prototype inheritance.
Prototype Chain
When JavaScript tries to access a property or method, it first checks the object itself.
If it doesn't find it, JavaScript looks at the object's prototype.
If it still doesn't find it, JavaScript continues searching up the prototype chain.
Conceptually:
Object
↓
Prototype
↓
Prototype's Prototype
↓
null
This is called the prototype chain.
Object.prototype
Most ordinary JavaScript objects ultimately inherit from:
Object.prototype
For example:
const user = {
name: "Saurabh"
};
console.log(
Object.getPrototypeOf(user)
);
The result is the object's prototype, which for an object literal is generally Object.prototype.
Object.getPrototypeOf()
We can inspect an object's prototype using:
Object.getPrototypeOf(user);
Example:
const user = {
name: "Saurabh"
};
console.log(
Object.getPrototypeOf(user)
);
This is useful for understanding the prototype chain.
The __proto__ Property
You may also encounter:
user.__proto__
This accesses the object's prototype through a legacy accessor.
Although you'll see it frequently while learning JavaScript, Object.getPrototypeOf() and Object.setPrototypeOf() are generally preferred for explicit prototype manipulation.
For example:
console.log(
Object.getPrototypeOf(user)
);
Constructor Functions
Before class syntax became common, JavaScript developers frequently used constructor functions to create similar objects.
Example:
function User(name, age) {
this.name = name;
this.age = age;
}
We can create objects using:
const user1 = new User(
"Saurabh",
22
);
const user2 = new User(
"Rahul",
23
);
The new keyword connects these objects to:
User.prototype
Adding Methods to a Prototype
Instead of creating a new copy of a method for every instance, we can place the method on the constructor's prototype.
User.prototype.greet = function () {
console.log(
`Hello ${this.name}`
);
};
Now:
user1.greet();
user2.greet();
Both objects can use the same prototype method.
Why Use the Prototype?
Consider this:
function User(name) {
this.name = name;
this.greet = function () {
console.log(
`Hello ${this.name}`
);
};
}
Every time we create a new User, a new function is created for greet.
Instead:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
console.log(
`Hello ${this.name}`
);
};
Now the method is shared through the prototype.
This is one of the important ideas behind JavaScript's prototype system.
Checking the Prototype
We can verify the relationship:
console.log(
Object.getPrototypeOf(user1) === User.prototype
);
Output:
true
This tells us that user1 is linked to User.prototype.
Prototype Chain Lookup
Suppose:
const user = {
name: "Saurabh"
};
We run:
user.toString();
JavaScript roughly searches like this:
Does user have toString?
↓
No
↓
Does user.__proto__ have toString?
↓
Yes
↓
Execute it
This lookup process is fundamental to understanding JavaScript inheritance.
Constructor's Prototype Property
Functions used as constructors have a .prototype property.
Example:
function User(name) {
this.name = name;
}
console.log(User.prototype);
We can add methods:
User.prototype.greet = function () {
console.log(
`Hello ${this.name}`
);
};
When an object is created with:
const user = new User("Saurabh");
the object's prototype is connected to:
User.prototype
Class Syntax and Prototypes
This is where yesterday's lesson connects with today's.
When we write:
class User {
greet() {
console.log("Hello");
}
}
the method isn't simply copied separately into every instance.
The class's methods are placed on:
User.prototype
So:
const user = new User();
console.log(
Object.getPrototypeOf(user) === User.prototype
);
returns:
true
This is why understanding prototypes helps explain what JavaScript classes are actually doing.
Prototypal Inheritance
Objects can inherit from other objects.
One way to create such a relationship is:
const animal = {
speak() {
console.log("Animal sound");
}
};
Create another object based on it:
const dog = Object.create(animal);
Now:
dog.speak();
The dog object doesn't have its own speak() method.
JavaScript finds it through its prototype.
Object.create()
Object.create() allows us to create a new object with a specified prototype.
Example:
const vehicle = {
start() {
console.log("Vehicle started");
}
};
const car = Object.create(vehicle);
car.start();
The prototype relationship is:
car
↓
vehicle
↓
Object.prototype
↓
null
Prototype Inheritance With Constructors
We can also create inheritance between constructor prototypes.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(
`${this.name} makes a sound`
);
};
Now create a Dog constructor:
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Connect the prototypes:
Dog.prototype = Object.create(
Animal.prototype
);
Dog.prototype.constructor = Dog;
Now add a Dog-specific method:
Dog.prototype.bark = function () {
console.log(
`${this.name} says Woof!`
);
};
Create the object:
const dog = new Dog(
"Bruno",
"Labrador"
);
dog.speak();
dog.bark();
The dog can access methods from the Animal prototype.
hasOwnProperty()
Sometimes we need to determine whether a property belongs directly to an object or is inherited.
Example:
const user = {
name: "Saurabh"
};
console.log(
user.hasOwnProperty("name")
);
Output:
true
But:
console.log(
user.hasOwnProperty("toString")
);
returns:
false
because toString is inherited rather than an own property.
Own Properties vs Inherited Properties
Consider:
const user = {
name: "Saurabh"
};
Own property
name
belongs directly to user.
Inherited property
toString
comes from the prototype chain.
This distinction becomes useful when inspecting and debugging objects.
Prototype vs __proto__
These two terms are often confused.
.prototype
Usually refers to the property on a constructor function that becomes the prototype of instances created with new.
User.prototype
[[Prototype]]
The internal prototype link of an object.
We can access it using:
Object.getPrototypeOf(user)
The legacy:
user.__proto__
also exposes that link, but it is generally better to use the standard Object.getPrototypeOf() API.
Best Practices
✔ Understand prototypes even if you primarily use class syntax.
✔ Prefer Object.getPrototypeOf() over relying on __proto__.
✔ Use prototypes for shared behavior when working with constructor functions.
✔ Don't modify built-in prototypes such as Array.prototype in normal application code.
✔ Use class syntax when it makes your application's object model easier to understand.
✔ Remember that JavaScript's class syntax is built on the prototype system.
My Biggest Takeaway
Today, I learned what's happening underneath JavaScript classes.
The biggest realization was:
JavaScript classes are built on top of prototypes.
When I create an instance:
const user = new User();
the object can access methods through:
user
↓
User.prototype
↓
Object.prototype
↓
null
Understanding this prototype chain makes JavaScript's inheritance model much clearer.
