A prototype, in terms of JavaScript, is inbuilt functionality that has a unique feature for object inheritance referred to as the prototype
JavaScript is a dynamically typed language and employs a unique mechanism for object inheritance: prototypes. Unlike classical inheritance found in languages like Java or C++, JavaScript utilises prototype-based inheritance, a powerful and flexible approach that can be both elegant and initially confusing. We will explore and unravel the intricacies of JavaScript prototypes, exploring their functionality, advantages and potential disadvantages.
Before diving into prototypes, it’s crucial to understand the fundamental building blocks of JavaScript: objects. Objects are collections of key-value pairs, where keys are typically strings (or Symbols) and values can be any JavaScript data type – numbers, strings, booleans, other objects, or even functions and these key-value pairs are called properties.
const myObject = {
name: "Alice",
age: 30,
city: "New York",
greet: function() {
console.log(`Hello, my name is ${this.name}!`);
}
};
console.log(myObject.name); // Accessing a property
myObject.greet(); // Calling a method (function property)
Every object in JavaScript has a hidden property called [[Prototype]] (often referred to as __proto__ in older implementations, though directly accessing it is discouraged). This property points to another object which serves as the prototype for the original object. Think of the prototype as a blueprint or template from which the object inherits properties and methods.
When you try to access a property of an object, JavaScript first checks if the object itself has that property. If it does, the value is returned. However, if the property isn’t found directly on the object, JavaScript’s engine follows the [[Prototype]] link to the prototype object. It then checks if the prototype object has the desired property. This process continues up the prototype chain until either the property is found or the end of the chain (which is null) is reached.
There are several ways to create objects in JavaScript, each influencing how prototypes are assigned:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}.`);
};
const john = new Person("John", 25);
john.greet(); // Accessing a method from the prototype
In this example, Person.prototype is the prototype for all objects created using the Person constructor. john inherits the greet method from Person.prototype.
const parent = {
name: "Jane",
sayHi: function() { console.log("Hi!"); }
};
const child = Object.create(parent);
child.name = "David"; // Overriding the parent's name property
child.sayHi(); // Inheriting the sayHi method
console.log(child.name); // Accessing the overridden property
Here, child’s prototype is parent. child inherits sayHi but overrides the name property.
The [[Prototype]] link forms a chain, known as the prototype chain. When you access a property, JavaScript traverses this chain until it finds the property or reaches the end (where the prototype is null). This is how inheritance works in JavaScript. Objects inherit properties and methods from their prototypes, and those prototypes can inherit from other prototypes creating a hierarchy.
While JavaScript’s prototype-based inheritance is different from classical inheritance, it’s possible to simulate some aspects of classical inheritance using prototypes. However, this is often discouraged as it can add complexity and obscure the true nature of JavaScript’s prototype system. It’s generally better to embrace the prototype-based approach head on.
ECMAScript 2015 (ES6) introduced the class syntax, which provides a more familiar way to define objects and their prototypes. Under the hood, the class syntax still uses prototypes; it’s just syntactic sugar that makes it look more like classical inheritance.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
bark() {
console.log("Woof!");
}
}
const myDog = new Dog("Buddy");
myDog.speak(); // Inherited from Animal
myDog.bark(); // Defined on Dog
The extends keyword in the class syntax establishes the prototype relationship. Dog’s prototype is Animal.prototype, allowing myDog to inherit the speak method.
JavaScript prototypes are a fundamental and powerful feature of the language. Understanding how prototypes work is essential for mastering JavaScript’s object model and effectively utilizing inheritance.
While initially confusing, the flexibility and efficiency of prototype-based inheritance make it a valuable tool for building complex and maintainable JavaScript applications. By embracing the prototype chain and understanding its nuances the full potential of JavaScript’s object-oriented capabilities can be realised.
__proto__ is a property on an object instance that points to the object’s prototype. It’s a way to access the internal [[Prototype]] property. prototype, on the other hand, is a property of a constructor function. It’s used to define the prototype for objects created using that constructor. In essence, __proto__ links an object to its prototype, while prototype is used to set the prototype for future objects created by a constructor. Directly accessing __proto__ is discouraged; the latest versions of JavaScript use Object.getPrototypeOf() and Object.setPrototypeOf() instead.
The prototype chain is a sequence of objects linked together by their [[Prototype]] (or __proto__ in older implementations) properties. When you try to access a property on an object JavaScript first checks if the object has that property. If not, it follows the [[Prototype]] link to the next object in the chain (the object’s prototype) and checks if that object has the property. This process continues up the chain until either the property is found or the end of the chain (which is null) is reached. If the property is not found after traversing the entire chain, undefined is returned. This is how inheritance is implemented in JavaScript.
Yes, you can modify the prototype of an existing object using Object.setPrototypeOf() or, in older environments, by directly manipulating __proto__ (though, again, this is discouraged). However, modifying prototypes, especially shared prototypes, should be done carefully and with caution. If multiple objects share the same prototype, changing the prototype will affect all of those objects. This can be useful for adding or modifying shared functionality, but it can also lead to unexpected behaviour if not carefully managed. It’s often better to establish the prototype chain correctly when creating the objects initially, rather than modifying it later.