JavaScript Object-Oriented Programming: Prototypes

JavaScript Object-Oriented Programming: Prototypes

JavaScript’s object-oriented programming is built on a unique and powerful concept called prototypes. Unlike many classical languages that use classes, JavaScript uses prototypes to enable objects to inherit properties and methods from other objects. This system allows objects to share behavior efficiently without duplicating code for each instance. Understanding prototypes is key to mastering how JavaScript organizes and reuses functionality in object-oriented designs.

At its core, every JavaScript object has a prototype — a hidden link to another object from which it can inherit properties and methods. When you try to access a property on an object, JavaScript first looks at that object’s own properties. If it doesn’t find it, it follows the prototype link to check the prototype object, continuing up the chain until it either finds the property or reaches the end. This chain of prototypes is known as the prototype chain, and it forms the foundation of JavaScript’s inheritance model.

Accessing and Setting an Object’s Prototype

To start working with prototypes, it helps to know how to access and change an object’s prototype. JavaScript provides Object.getPrototypeOf() to check an object’s prototype, and Object.setPrototypeOf() to change it. There is also a less formal way, the __proto__ property, which behaves similarly.

Consider this simple example where we create a plain object and inspect its prototype:

const animal = { eats: true };
console.log(Object.getPrototypeOf(animal)); // Output: {}

const objProto = Object.getPrototypeOf(animal);
console.log(objProto === Object.prototype); // Output: true

Here, animal is a simple object with one property, eats. When we get its prototype, we find it points to Object.prototype, the root prototype that most objects inherit from. This confirms that animal inherits properties and methods from the base object prototype.

Adding Methods to an Object’s Prototype

When using constructor functions, prototypes become very useful. Each constructor has a .prototype property. By adding methods to this property, all instances created from that constructor share those methods. This avoids repeating the same function for every object.

Let’s define a constructor for Cat objects and add a method meow to its prototype:

function Cat(name) {
  this.name = name;
}

Cat.prototype.meow = function() {
  console.log(this.name + " says Meow!");
};

const felix = new Cat("Felix");
felix.meow(); // Output: Felix says Meow!

const garfield = new Cat("Garfield");
garfield.meow(); // Output: Garfield says Meow!

The method meow is only defined once on Cat.prototype. Both felix and garfield can call it, and this inside the method refers to the calling object, allowing each cat to say its own name.

Prototype Chain in Action

JavaScript uses the prototype chain to look up properties not found directly on the object. When you try to access a property, the engine searches the object itself, then its prototype, then that prototype’s prototype, and so forth until it finds the property or reaches null.

Let’s see how this works with an example:

const parent = {

  walk: function () {
    console.log("Walking...");
  },

};

const child = Object.create(parent);

child.walk(); // Output: Walking...

console.log(child.hasOwnProperty("walk")); // Output: false
console.log(parent.isPrototypeOf(child)); // Output: true

Here, child has no direct walk method, but its prototype, parent, does. Calling child.walk() works because the prototype chain leads to parent. The hasOwnProperty check confirms that walk is not directly on child, and isPrototypeOf verifies the prototype link. The Object.create method lets you create a new object with the specified object as its prototype, may be null. If the prototype is null, it means the new object does not inherit from any other object.

Overriding Prototype Properties and Methods

An important feature of prototypes is that objects can override inherited properties or methods by defining their own versions. When this happens, JavaScript uses the object’s own property instead of searching the prototype chain.

Let’s override the meow method for one specific Cat instance:

function Cat(name) {
  this.name = name;
}

Cat.prototype.meow = function() {
  console.log(this.name + " says Meow!");
};

const felix = new Cat("Felix");

const tom = new Cat("Tom");

tom.meow = function() {

  console.log(this.name + " growls instead of meowing!");

};

tom.meow();    // Output: Tom growls instead of meowing!
felix.meow();  // Output: Felix says Meow!

In this example, tom replaces the shared meow method with its own version. Calling tom.meow() triggers the overridden function, while felix still uses the prototype’s original method. This shows how instances can have unique behavior while sharing a common prototype.

Using Object.create() to Set Prototypes

Another way to set an object’s prototype is by using Object.create(). This method creates a new object with a specified prototype without needing a constructor function. It’s a straightforward way to build prototype chains manually.

Let’s create a base prototype with shared behavior and create objects inheriting from it:

const animalProto = {

  speak: function() {
    console.log(this.name + " makes a sound.");
  }

};

const dog = Object.create(animalProto);
dog.name = "Rex";
dog.speak(); // Output: Rex makes a sound.

Here, dog is created with animalProto as its prototype. It can call speak even though it does not define it directly. This method offers a clean way to build prototypes without constructors.

Prototypes in Built-in JavaScript Objects

JavaScript’s built-in objects such as Array, String, and Number also use prototypes to share methods. You can even add your own methods to these prototypes.

For example, let’s add a custom method to Array.prototype:

Array.prototype.first = function() {
  return this[0];
};

const fruits = ["Mango", "Banana", "Papaya"];
console.log(fruits.first()); // Output: Mango

All arrays now have access to the first method, which returns the first element. This example highlights how prototype chains allow powerful extensions of built-in objects.

Fun Example: A Family of Animals with Shared Behaviors

To wrap up, let’s build a lively example that uses prototypes to model a family of animals. We’ll create a shared Animal prototype and then create Dog and Cat objects inheriting from it with some overrides.

const Animal = {

  speak: function() {
    console.log(this.name + " makes a noise.");
  },
  eat: function() {
    console.log(this.name + " is eating.");
  }

};

const dog = Object.create(Animal);
dog.name = "Buddy";
dog.speak = function() {
  console.log(this.name + " barks loudly!");
};

const cat = Object.create(Animal);
cat.name = "Whiskers";

dog.speak();  // Output: Buddy barks loudly!
dog.eat();    // Output: Buddy is eating.

cat.speak();  // Output: Whiskers makes a noise.
cat.eat();    // Output: Whiskers is eating.

The dog overrides speak while cat uses the inherited version. Both share the eat method from Animal. This simple family shows prototypes at work creating shared and unique behavior in objects.

Conclusion

Prototypes are the backbone of JavaScript’s object system. They allow objects to inherit properties and methods, enabling efficient code reuse. We explored how to access and set prototypes, add shared methods, and how property lookup happens along the prototype chain. We also saw how to override prototype properties and create objects with Object.create(). Finally, our animal family example illustrated prototypes in a fun, practical way.

Mastering prototypes unlocks deeper understanding of JavaScript’s inheritance and is essential for working effectively with objects.

References

If you want to learn more about JavaScript prototypes and inheritance, these resources are excellent starting points:

Scroll to Top