JavaScript Prototypes: From Beginner to Pro

JavaScript
5 minutes read

Introduction

JavaScript is a fascinating language, often described as a prototype-based language. But what does this mean? In this article, we’ll unpack the concept of prototypes, how they facilitate inheritance in JavaScript, and much more. Whether you’re a beginner or an advanced developer this article is for you.

What is a Prototype in JavaScript?

In JavaScript, virtually everything is an object – an entity that binds keys to values. Each object we create is automatically linked to another object, termed its prototype. This prototype acts like a template, setting its properties and methods to the object linked with it. This phenomenon forms the basis of JavaScript’s powerful prototype-based inheritance model.

When we attempt to access a property that doesn’t exist in the object, JavaScript doesn’t just throw up its hands in defeat. Instead, it moves into the object’s prototype chain, a series of prototypes linked together, trying to find this property. If the property still isn’t found, it continues this process up the chain until it reaches the null object, signaling the end of the prototype chain.

Simple Prototyping

Let’s break down how we can use prototypes in JavaScript:

function Vehicle(type, wheels) {
    this.type = type;
    this.wheels = wheels;
}

Vehicle.prototype.describe = function() {
    return `A ${this.type} has ${this.wheels} wheels`;
}

let car = new Vehicle("car", 4);
console.log(car.describe()); // Outputs: "A car has 4 wheels"

In the code snippet above:

  • We establish a Vehicle function, our constructor for vehicles.
  • We enrich the Vehicle prototype with a describe function. This means any object born from new Vehicle() will now possess this method.

We initiate a new car object through the Vehicle constructor and call its describe method.

This interaction showcases the power of prototypes, allowing us to build objects with shared functionality, thereby promoting code reusability and efficiency.

Advanced Prototyping

Having covered the basics, let’s now explore some advanced concepts related to JavaScript prototypes:

Prototype Chain

The prototype chain is a vital concept to grasp in JavaScript. When you try to access a method or property on an object, JavaScript first tries to find the property on the object itself. If it’s not found, the search continues onto the object’s prototype and then the prototype’s prototype, and so on, up the prototype chain until it either discovers the property/method or it reaches the end of the chain.

Here is an example of a prototype chain in JavaScript. Let’s use a simple example of animals to illustrate this concept:

// Define the Animal prototype
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    return `${this.name} is eating`;
}

// Define the Bird prototype, which inherits from Animal
function Bird(name) {
    Animal.call(this, name);
}

Bird.prototype = Object.create(Animal.prototype); // Set up inheritance from Animal
Bird.prototype.constructor = Bird; // Set constructor back to Bird

Bird.prototype.fly = function() {
    return `${this.name} is flying`;
}

// Create a new Bird object
let bird = new Bird("Sparrow");

console.log(bird.eat()); // Outputs: "Sparrow is eating"
console.log(bird.fly()); // Outputs: "Sparrow is flying"

In this code:

  • We create an Animal function, which acts as the constructor for animals. It has a name property and an `eat` method, which all animals have.
  • We then create a Bird function. Birds are a specific kind of animal, so they should inherit from Animal. We call the Animal function from within Bird to initialize the name property, and then set up the prototype chain with Object.create(Animal.prototype). This means that if JavaScript can’t find a property on the Bird object, it will look in Animal.prototype.
  • We add a fly method to Bird.prototype. This method is specific to birds and is not shared with other animals.
  • Finally, we create a bird object from the Bird constructor and then call its eat and fly methods. Both of these methods are found by JavaScript going up the prototype chain: eat is found on Animal.prototype, while fly is found on Bird.prototype.

Object.create()

JavaScript provides Object.create() to generate an object with a specific prototype. Let’s demonstrate this:

let vehicle = {
    wheels: 4,
    describe: function() {
        return `This vehicle has ${this.wheels} wheels`;
    }
};

let car = Object.create(vehicle);
console.log(car.describe()); // Outputs: "This vehicle has 4 wheels"

Object.prototype

Object.prototype forms the root of the prototype chain. It provides methods and properties common to all objects, such as .toString() and .valueOf().

Let’s take an example

let food = {
    name: "Apple",
};

console.log(food.toString()); // Outputs: "[object Object]"

let emptyObject = Object.create(null);
console.log(emptyObject.toString()); // Uncaught TypeError: emptyObject.toString is not a function

In the above code:

  • We created a food object. Even though we didn’t define a toString() method, we can call toString() on food. That’s because food inherits from Object.prototype, which has a toString() method.
  • Object.create(null) creates an object that doesn’t inherit from anything, not even Object.prototype. As a result, this object does not have a toString() method, and trying to call it results in a TypeError.

We can add methods to Object.prototype, and they’ll become available to all objects. However, this is generally considered bad practice, because it can lead to conflicts with other code and built-in methods. Here’s an example, but remember, modifying Object.prototype in real-world code isn’t usually a good idea:

Object.prototype.greet = function() {
    return `Hello, I'm an object!`;
}

let myObject = {};
console.log(myObject.greet()); // Outputs: "Hello, I'm an object!

In this code, we added a greet method to Object.prototype. Now every object can call greet(), even though we didn’t define this method on myObject specifically.

Prototype Pollution

Prototype pollution is when properties on a prototype are modified by mistake or intentionally, which can lead to changes in all objects that inherit from that prototype. This can be a serious issue as it may lead to security vulnerabilities or bugs that can be hard to trace.

Here’s an example of prototype pollution:

let user1 = { name: 'Alice' };
let user2 = { name: 'Bob' };

console.log(user1.isAdmin); // Outputs: undefined
console.log(user2.isAdmin); // Outputs: undefined

Object.prototype.isAdmin = true;

console.log(user1.isAdmin); // Outputs: true
console.log(user2.isAdmin); // Outputs: true

In this code, we first log the isAdmin property of user1 and user2, which are both undefined. Then we add the isAdmin property to Object.prototype and set it to true. This pollutes the prototype, and now when we log the isAdmin property of user1 and user2 again, they are both true.

This is a simple example, but you can imagine how this could lead to bugs or security issues. For instance, if a user could control the keys or values being added to an object, they might be able to add properties to Object.prototype and change the behavior of your code in unexpected ways.

As such, it’s generally considered best practice to avoid modifying objects’ prototypes, particularly Object.prototype.

__proto__ vs prototype

  • __proto__: This is an actual object within each instance that points to the prototype of the object.
  • prototype: This is an object that is associated with a function and is involved in the process of creating new instances. A prototype is an object that is assigned to the __proto__ of the new instance.

Let’s break this down with an example:

// Constructor function
function Car(make, model) {
    this.make = make;
    this.model = model;
}

Car.prototype.displayCar = function() {
    return `${this.make} ${this.model}`;
}

let myCar = new Car("Toyota", "Corolla");

console.log(myCar.displayCar()); // Outputs: "Toyota Corolla"

In this example:

  • Car.prototype is the prototype object associated with the function Car(). It has a property displayCar which is a function.
  • myCar is an instance of Car. When we call new Car(), an object is created, and its __proto__ property is set to the value of Car.prototype.
  • myCar.__proto__ is the same object as Car.prototype. This means that we can call myCar.displayCar(), and JavaScript will find the displayCar function on the prototype.

It’s also worth mentioning that the __proto__ property is non-standard and should not be used in production code. For practical purposes, you should use Object.getPrototypeOf(obj) to get the prototype of an object and Object.setPrototypeOf(obj, prototype) to set it.

Further Reading

Conclusion

In conclusion, understanding JavaScript prototypes is fundamental for any JavaScript developer. From the basic notion of prototypes to advanced concepts like the prototype chain, Object.create, Object.prototype, and even potential pitfalls like prototype pollution, these concepts form the basis of JavaScript’s object-oriented approach. It’s also crucial to understand the differences between __proto__ and prototype, and when to use each. Mastering these topics will not only make you a more effective developer but will also empower you to make the most of JavaScript’s robust and flexible design. And as always, the key to learning these complex concepts is practice. So, keep experimenting, keep coding, and happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *