Introduction to Prototypes in JavaScript

Introduction to Prototypes in JavaScript

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
  1. constructor: This property points back to the function or class that created the prototype.

  2. toString(): This method returns a string representing the object.

  3. 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:

  1. Create a new empty object.

  2. Link this object to another object. This linking is done to the function’s prototype (i.e., this.__proto__ = Function.prototype).

  3. Assign this to the newly created object.

  4. 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!