Prototype and Prototypal Inheritance in JavaScript
JavaScript is often described as a prototype-based language. JavaScript uses prototypal inheritance, unlike traditional object-oriented programming languages that use class-based inheritance. This concept is a cornerstone of JavaScript and helps developers understand how objects, functions, and inheritance work.
In this article, we’ll explore what prototypes are, how prototypal inheritance works in JavaScript, and how it differs from classical inheritance.
What is a Prototype?
In JavaScript, every object has an internal property called [[Prototype]]
, which refers to another object. This object is called the prototype. The prototype provides a mechanism by which objects can inherit properties and methods from other objects.
When you try to access a property or method on an object, JavaScript first looks at that object. If the property or method is not found on the object itself, JavaScript will look at its prototype (and continue up the prototype chain) until it finds the property or reaches the end of the chain (null).
Example of Prototypal Chain:
const animal = {
eats: true
};
const dog = {
barks: true
};
// Set animal as the prototype of dog
dog.__proto__ = animal;
console.log(dog.barks); // Output: true (from dog object)
console.log(dog.eats); // Output: true (inherited from animal)
In this example, dog
doesn’t have the eats
property, so JavaScript looks at the prototype (animal
) and finds it there. This is the essence of prototypal inheritance.
How Prototypal Inheritance Works
In JavaScript, objects can inherit properties and methods from other objects through their prototype. This allows for property and method sharing across different objects. Prototypal inheritance enables JavaScript to reuse properties and methods without explicitly defining them for every object.
When JavaScript looks for a property or method:
- It first checks the object itself.
- If the property or method is not found, it checks the object's prototype.
- If the prototype doesn’t have the property or method, the search continues up the prototype chain.
- If the chain ends (i.e., the prototype is
null
), and the property or method is still not found, JavaScript returnsundefined
.
Example:
const vehicle = {
wheels: 4,
drive() {
console.log('Driving...');
}
};
const car = {
brand: 'Toyota'
};
// Set vehicle as the prototype of car
car.__proto__ = vehicle;
console.log(car.brand); // Output: Toyota (own property of car)
console.log(car.wheels); // Output: 4 (inherited from vehicle)
car.drive(); // Output: Driving... (inherited from vehicle)
Here, the car
object inherits properties and methods from the vehicle
object. This is because the car
object’s prototype points to vehicle
, creating an inheritance relationship.
The Object.prototype
At the top of the prototype chain is Object.prototype
. This is the base object from which all other objects in JavaScript inherit. For example, all objects in JavaScript have access to methods like toString()
and hasOwnProperty()
because they are defined on Object.prototype
.
const obj = {};
console.log(obj.toString()); // Output: [object Object]
console.log(obj.hasOwnProperty('key')); // Output: false
In this case, toString()
and hasOwnProperty()
are not defined on obj
, but JavaScript finds them on Object.prototype
.
Constructor Functions and Prototypes
In JavaScript, constructor functions are a common way to create objects, and they play a key role in prototypal inheritance. Constructor functions are special functions used to create and initialize objects, and every function in JavaScript has a prototype
property, which defines what will be inherited by instances created by that constructor.
Example:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const john = new Person('John', 30);
const jane = new Person('Jane', 25);
john.greet(); // Output: Hello, my name is John
jane.greet(); // Output: Hello, my name is Jane
In this example:
Person
is a constructor function.- Each new instance of
Person
(likejohn
andjane
) has access to thegreet
method because it’s defined onPerson.prototype
.
How the Prototype Chain Works with Constructors
Every object created using a constructor function has its [[Prototype]]
linked to the constructor’s .prototype
object. This is how JavaScript sets up the inheritance relationship.
- The
john
andjane
objects inherit fromPerson.prototype
. Person.prototype
is also an object, so it inherits fromObject.prototype
.
The prototype chain for the john
object looks like this:
john --> Person.prototype --> Object.prototype --> null
Object.create()
for Inheritance
The Object.create()
method allows you to create a new object with a specific prototype, without the need for a constructor function.
Example:
const animal = {
eats: true
};
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.barks); // Output: true
console.log(dog.eats); // Output: true (inherited from animal)
In this case, the dog
object is created with animal
as its prototype. This is a more straightforward way to create inheritance relationships without using constructor functions.
Prototypal Inheritance vs Classical Inheritance
In classical (class-based) inheritance, objects are instantiated from classes, and inheritance occurs when one class inherits from another. Most traditional object-oriented languages like Java, C++, and C# follow this model.
JavaScript, however, uses prototypal inheritance, where objects can directly inherit from other objects without needing classes. Each object can have a prototype, and properties and methods are shared along the prototype chain.
With the introduction of ES6, JavaScript introduced the class
syntax, which is just syntactic sugar over JavaScript's prototypal inheritance.
Example of ES6 class
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
const john = new Person('John', 30);
john.greet(); // Output: Hello, my name is John
While this looks like classical inheritance, behind the scenes, JavaScript still uses prototypal inheritance. The class
syntax makes it easier for developers coming from traditional OOP languages to work with JavaScript, but the underlying mechanism remains the same.
Modifying Prototypes
You can modify prototypes at any time. This allows you to add new methods to an object’s prototype, and all instances of that object will automatically have access to the new methods.
Example:
Person.prototype.sayAge = function() {
console.log(`I am ${this.age} years old.`);
};
john.sayAge(); // Output: I am 30 years old
jane.sayAge(); // Output: I am 25 years old
By adding a sayAge
method to Person.prototype
, both john
and jane
can now use it, even though it was added after the instances were created.
The __proto__
vs prototype
__proto__
: This is the internal property of an object that points to its prototype.prototype
: This is a property of constructor functions, used to set the prototype for objects created using thenew
keyword.
For example:
john.__proto__ === Person.prototype; // true
john.__proto__
points to Person.prototype
, creating the inheritance relationship.
Summary of Prototypal Inheritance
- Every object in JavaScript has a prototype, which is another object that it inherits properties and methods from.
- Prototypal inheritance allows objects to share functionality without duplicating code.
- Constructor functions and the
new
keyword are commonly used to create objects with prototypes. - ES6
class
syntax is syntactic sugar over prototypal inheritance. - Objects inherit from other objects through the prototype chain, allowing JavaScript to reuse properties and methods across instances.
Prototypal inheritance is a flexible and powerful system that sets JavaScript apart from traditional class-based languages. By understanding prototypes, you can unlock a deeper understanding of how JavaScript works under the hood and write more efficient, modular code.