JavaScript is a prototype-based object-oriented language, which means that inheritance and code reuse are managed through a prototype system.
The prototype allows objects to inherit properties and methods from other objects. This inheritance mechanism is called prototypical inheritance.
This allows objects to share methods and properties, which optimizes memory usage.
This is a different approach from other languages that use a class structure, and is a result of JavaScript’s dynamic typing nature (and it has its advantages and disadvantages).
Moreover, it is one of the most challenging points to understand in the language, so let’s take a closer look 👇.
What is a prototype
The prototype is simply an object that all objects have as an internal property, and to which they can access.
All objects in JavaScript have the property [[Prototype]]
, which in turn is a reference to another “parent” object.
This sequence of objects connected by prototypes is called the prototype chain.
When you try to access a property or method that does not exist on the object, JavaScript will look for it in the object’s prototype.
It will continue searching through the prototype chain until it finds the property or reaches a null
prototype (which generally means it has reached a base object).
Creating objects and prototypes
Let’s see at what point in the creation of an object the prototype is associated with the object.
Creation with Object Literals
When you create an object using literal notation, JavaScript automatically assigns Object.prototype
as the prototype of the new object:
const person = {
name: "Carlos",
age: 30
};
console.log(person.__proto__ === Object.prototype); // true
Creation with Object.create()
You can also create a new object with a specific prototype using Object.create(proto)
:
const animal = {
makeSound() {
console.log("Sound");
}
};
const dog = Object.create(animal);
dog.makeSound(); // "Sound"
In this case, dog
has animal
as its prototype, so it inherits the makeSound
method.
Constructor Function and Prototype
When we use a constructor function with the new
operator, a new object is created that inherits from that constructor function’s prototype
.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old!`);
};
// Create a new instance of Person
let person1 = new Person("Luis", 30);
// Call the `greet` method from the prototype
person1.greet(); // Output: Hello, my name is Luis and I am 30 years old!
In this example:
- Person is a constructor function that takes
name
andage
as parameters. - Person.prototype is an object where we can add methods and properties that will be shared by all instances of
Person
. - The
greet
method is added to the prototype ofPerson
, so all instances ofPerson
can access it.
Property proto
Generally, the prototype of an object is publicly available through the __proto__
property.
console.log(juan.__proto__ === Person.prototype); // true
Direct access to the __proto__
property is not recommended. Instead of using __proto__
, it is preferable to use the static methods provided by the Object
object to work with prototypes.
Object.getPrototypeOf(obj)
Gets the prototype of the object.
console.log(Object.getPrototypeOf(juan) === Person.prototype); // true
Object.setPrototypeOf(obj, proto)
Sets the prototype of the object.
const newProto = { newProperty: "value" };
Object.setPrototypeOf(juan, newProto);
console.log(juan.newProperty); // "value"
Modifying Prototypes
We can add methods to a prototype after instances have been created, which is useful for extending functionality.
Person.prototype.birthday = function() {
this.age++;
console.log(`Congratulations ${this.name}, now you are ${this.age} years old.`);
};
juan.birthday(); // Output: "Congratulations Luis, now you are 31 years old."
Modifying the prototype of native objects
One of the powerful features of JavaScript is that we can modify the prototypes of native objects, such as Array
, String
, or Object
. This allows us to add custom methods to basic types.
Array.prototype.printFirstElement = function() {
console.log(this[0]);
};
let myArray = [10, 20, 30];
myArray.printFirstElement(); // Shows: 10
In this example, we added the method printFirstElement
to the prototype of Array
. Now, all arrays in JavaScript will have this method available.
Although it is possible to modify the prototypes of native objects, it is generally not a good idea (in fact, it is often quite a bad idea)
Modifying the prototypes of native objects can generate unexpected conflicts, especially in large projects or when working with external libraries.
Prototypical Inheritance in JavaScript
Prototypical inheritance is what allows one object to inherit properties and methods from another object. This feature is at the core of JavaScript’s inheritance system.
For example, consider the following example, where we create two constructor functions: Animal
and Dog
. The Dog
function will inherit from Animal
via the prototype:
// Create a base object: Animal
let Animal = {
greet: function() {
console.log(`Hello, I am a ${this.type}`);
}
};
// Create a new object that inherits from Animal
let Dog = Object.create(Animal);
// Define specific properties for Dog
Dog.type = "dog";
Dog.bark = function() {
console.log("Woof!");
};
// Use the Dog object
Dog.greet(); // "Hello, I am a dog"
Dog.bark(); // "Woof!"
In this example:
Animal
is a base object with agreet
method.Dog
is created usingObject.create(Animal)
, which setsAnimal
as its prototype.Dog
has its own properties (type = "dog"
) and additional methods (bark
).- Since
Dog
inherits fromAnimal
, it can use thegreet
method defined inAnimal
.
Property Shadowing
If a property is defined on an object and also on its prototype, the object’s property “shadows” the prototype’s property.
This means that when accessing the property, the value from the object is obtained, not from the prototype.