Understanding Object-Oriented Programming (OOP)
In most traditional object-oriented programming languages like Java or C++, when you create a class and then create an object (an instance of that class), that object is a copy of the class blueprint. Any changes made to the class after the object has been created will not reflect in the object that is already in memory.
Example (Java-like syntax):
javaCopy codeclass Car {
String model;
String color;
Car(String model, String color) {
this.model = model;
this.color = color;
}
}
Car car1 = new Car("Toyota", "Red");
// Later, you add a new method to the Car class
class Car {
String model;
String color;
Car(String model, String color) {
this.model = model;
this.color = color;
}
void honk() {
System.out.println("Honk! Honk!");
}
}
// car1 does not have the honk() method, because it was created before the method was added
In this example, car1
does not have access to the honk()
method because it was instantiated before the method was added to the class.
How JavaScript Differs: The Prototype System
JavaScript operates differently from traditional OOP languages. Instead of copying a class blueprint, objects in JavaScript are linked to their prototypes. This allows for more dynamic changes and greater flexibility in how objects inherit properties and methods.
When you create an object in JavaScript, it's not an independent copy of a class but rather linked to a prototype object. This prototype can be shared among many objects, and changes to the prototype are reflected in all objects linked to it.
Example:
javascriptCopy codefunction Car(model, color) {
this.model = model;
this.color = color;
}
// Adding a method to the Car prototype
Car.prototype.honk = function() {
console.log("Honk! Honk!");
};
const car1 = new Car("Toyota", "Red");
car1.honk(); // Output: Honk! Honk!
// Adding another method to the prototype
Car.prototype.start = function() {
console.log(this.model + " is starting.");
};
car1.start(); // Output: Toyota is starting.
In this JavaScript example, car1
gains access to the honk()
and start()
methods because it is linked to Car.prototype
. Even after car1
has been created, you can add methods to the Car
prototype, and car1
will automatically have access to those methods.
The Prototype Chain: How Objects Are Linked
When you create a function in JavaScript, it comes with a built-in prototype object. This prototype object is automatically linked to Object.prototype
, which is the root prototype from which all objects in JavaScript ultimately inherit. This linking forms what is known as the "prototype chain."
Example:
javascriptCopy codefunction Bike(model, speed) {
this.model = model;
this.speed = speed;
}
Bike.prototype.ride = function() {
console.log(this.model + " is riding at " + this.speed + ".");
};
const bike1 = new Bike("Yamaha", "150mph");
bike1.ride(); // Output: Yamaha is riding at 150mph.
// Prototype Chain
console.log(bike1.__proto__ === Bike.prototype); // true
console.log(Bike.prototype.__proto__ === Object.prototype); // true
Here, bike1.__proto__
points to Bike.prototype
, and Bike.prototype.__proto__
points to Object.prototype
. This chain allows bike1
to inherit methods from both Bike.prototype
and Object.prototype
.
The Importance of the constructor
Property
Every prototype object in JavaScript has a constructor
property that points back to the function or class that created it. This is important because it creates a circular reference that links objects to their original constructor.
Example:
javascriptCopy codefunction Employee(name, position) {
this.name = name;
this.position = position;
}
Employee.prototype.work = function() {
console.log(this.name + " is working as a " + this.position + ".");
};
const emp1 = new Employee("Alice", "Developer");
// Checking the constructor property
console.log(Employee.prototype.constructor === Employee); // true
console.log(emp1.constructor === Employee); // true
In this example, Employee.prototype.constructor
points back to the Employee
function, and emp1.constructor
also points to Employee
, ensuring that objects can trace their origin back to their constructor.
Real-World Scenario: Prototype in Action
Let's consider a real-world scenario: Suppose you're building a simple e-commerce platform where customers can view and purchase products. Each product has basic properties like name, price, and category. However, you later decide to add a discount feature. In a traditional OOP language, you'd have to manually update each product object to include this new feature. But in JavaScript, you can simply add the discount method to the Product
prototype, and all existing product objects will automatically have access to it.
Example:
javascriptCopy codefunction Product(name, price, category) {
this.name = name;
this.price = price;
this.category = category;
}
Product.prototype.applyDiscount = function(discount) {
this.price = this.price - (this.price * (discount / 100));
console.log(this.name + " new price after discount is: " + this.price);
};
const product1 = new Product("Laptop", 1000, "Electronics");
product1.applyDiscount(10); // Output: Laptop new price after discount is: 900
// Adding a new method to the prototype
Product.prototype.showDetails = function() {
console.log(`Product: ${this.name}, Category: ${this.category}, Price: ${this.price}`);
};
product1.showDetails(); // Output: Product: Laptop, Category: Electronics, Price: 900
In this example, we add the applyDiscount
and showDetails
methods to the Product
prototype. Now, all existing and future product objects automatically gain these methods without any additional code.
Deep Dive into JavaScript Prototypes
The Structure of a Prototype
In the first part, we discussed how every function in JavaScript automatically gets a prototype
property, which is an object. This object has several properties that can be inherited by instances of that function.
Common Properties on prototype
constructor
: This property points back to the function or class that created the prototype.toString()
: This method returns a string representing the object.valueOf()
: This method returns the primitive value of the specified object.
Example:
javascriptCopy codefunction Car(model, color) {
this.model = model;
this.color = color;
}
Car.prototype.honk = function() {
console.log("Honk! Honk!");
};
const myCar = new Car("Toyota", "Red");
console.log(myCar.constructor === Car); // true
console.log(myCar.toString()); // "[object Object]"
console.log(myCar.valueOf()); // Car { model: 'Toyota', color: 'Red' }
In this example, myCar
has access to constructor
, toString()
, and valueOf()
methods, even though they weren’t explicitly defined in the Car
constructor. This is because these methods are part of the prototype chain.
The Role of Object.prototype
When we create any function in JavaScript, its prototype is linked to Object.prototype
, which is the root of the prototype chain. This means that all objects in JavaScript ultimately inherit properties and methods from Object.prototype
, like toString()
, hasOwnProperty()
, and valueOf()
.
Example:
javascriptCopy codefunction Animal(name) {
this.name = name;
}
const dog = new Animal("Rex");
console.log(dog.hasOwnProperty('name')); // true
console.log(dog.toString()); // "[object Object]"
Here, dog
inherits the hasOwnProperty()
and toString()
methods from Object.prototype
because Animal.prototype
is linked to Object.prototype
.
The new
Keyword and Object Creation
Unlike other object-oriented languages, in JavaScript, when you create an object using the new
keyword, the object creation process is more nuanced. The new
keyword follows a four-step process:
Create a new empty object.
Link this object to another object. This linking is done to the function’s prototype (i.e.,
this.__proto__ = Function.prototype
).Assign
this
to the newly created object.Return
this
if the function does not explicitly return an object.
Example:
javascriptCopy codefunction Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
const person1 = new Person("John");
person1.sayHello(); // Output: Hello, my name is John
In this example, when person1
is created using new
, it goes through the four phases: creating a new object, linking it to Person.prototype
, assigning this
to it, and returning the new object.
Linking and Prototype Chaining
The crucial second step in the new
keyword process is linking the new object to another object via the prototype. This is where JavaScript’s prototype chain comes into play.
When an object is created, it doesn’t contain a copy of the methods or properties defined on its prototype. Instead, it’s linked to the prototype object. When you access a property or method, JavaScript first looks at the object itself. If it doesn’t find the property, it looks up the prototype chain.
Example:
javascriptCopy codefunction Gadget(name, brand) {
this.name = name;
this.brand = brand;
}
Gadget.prototype.getDetails = function() {
return this.name + " by " + this.brand;
};
const gadget1 = new Gadget("Smartphone", "BrandX");
console.log(gadget1.getDetails()); // Output: Smartphone by BrandX
console.log(gadget1.hasOwnProperty('name')); // true
console.log(gadget1.hasOwnProperty('getDetails')); // false (inherited from prototype)
Here, getDetails()
is not found directly on gadget1
, so JavaScript looks up the prototype chain and finds it on Gadget.prototype
.
Real-World Scenario: Prototype in Web Development
Imagine you’re developing a website that tracks users' activities. You create a User
class that records basic information like name and activity. Later, you realize you need to add a new method to track login times. Instead of updating each existing user object, you can simply add this method to User.prototype
, and all current and future user objects will have access to it.
Example:
javascriptCopy codefunction User(name) {
this.name = name;
this.activity = [];
}
User.prototype.addActivity = function(activity) {
this.activity.push(activity);
};
User.prototype.showActivities = function() {
console.log(this.name + "'s activities: " + this.activity.join(", "));
};
// Adding a new method to the prototype
User.prototype.trackLogin = function(time) {
this.activity.push("Login at " + time);
};
const user1 = new User("Alice");
user1.addActivity("Viewed Profile");
user1.trackLogin("10:00 AM");
user1.showActivities(); // Output: Alice's activities: Viewed Profile, Login at 10:00 AM
In this scenario, you added the trackLogin()
method after creating the User
class. All existing User
objects, like user1
, automatically gain this new functionality, showcasing the power of prototypes in JavaScript.
Importance of Prototypes
Prototypes are central to JavaScript’s inheritance model. They enable objects to share properties and methods, conserve memory, and create more efficient code. By understanding and leveraging prototypes, you can create dynamic, flexible, and maintainable JavaScript applications.
Summary of Part 1 and Part 2: Understanding JavaScript Prototypes
In the first part of this blog series, we laid the foundation for understanding JavaScript prototypes. We began by comparing JavaScript's prototype-based object model to the class-based models in other languages, highlighting the differences and the unique approach JavaScript takes. We explored the concept of prototypes as an integral part of how objects and functions in JavaScript are linked, with a focus on how every function in JavaScript has a prototype
property, and how this mechanism allows for the extension and modification of object properties even after they have been created.
In the second part, we dove deeper into the practical aspects of prototypes. We discussed the structure of the prototype
object, the role of common properties like constructor
, and how Object.prototype
serves as the root of the prototype chain. We also examined how the new
keyword works in JavaScript, breaking down the four-step process of object creation, with a particular focus on the linking phase where the new object is connected to its prototype. We then discussed how prototype chaining works, enabling objects to inherit properties and methods from their prototypes, and provided real-world examples of how this feature is used in web development.
Overall Summary
JavaScript prototypes are a powerful and fundamental feature of the language, enabling dynamic inheritance and efficient memory usage. Unlike traditional class-based inheritance found in other languages, JavaScript uses a prototype-based model that allows objects to be linked to each other, forming a prototype chain. This chain allows properties and methods to be shared across objects, making JavaScript both flexible and efficient.
By understanding how prototypes work—from the initial creation of objects to the nuances of prototype chaining—you can write more effective and maintainable JavaScript code. Whether you’re extending existing functionality or creating complex applications, mastering prototypes is key to unlocking the full potential of JavaScript.
Next in the Series: Prototypal Inheritance
In the next blog, we will explore the concept of prototypal inheritance in JavaScript. We'll discuss how objects can inherit properties and methods from other objects, delve into the details of how inheritance works in a prototype-based language, and examine how you can leverage this feature to create more complex and reusable code structures. Stay tuned!