Inheritance and Prototypes in JavaScript
go backGo back

Inheritance and Prototypes in JavaScript

Published on Jan 19 2023

Last updated on Jan 20 2023

Image by Karine Avetisyan on Unsplash
No translation available.
Add translation

JavaScript is a prototype-based language, differs from Java, C++, Python, or PHP which are class-based languages. JavaScript has only dynamic types and no static types, which makes it a unique language among others. In JavaScript, object's methods and properties are shared through a generalized object that can be cloned and extended. This is called prototypical inheritance.

What is prototypical inheritance?

In JavaScript, the concept of prototypical inheritance refers to the way in which objects inherit properties and methods from other objects. Just like how your kids always seem to inherit your bad habits, but not your good ones.

Every object in JavaScript has a prototype, which is another object that it inherits properties and methods from. When an object is created, it inherits the properties and methods of its prototype, and can also add its own properties and methods.

The prototype chain

An object can also have a prototype of its own, and this prototype can have a prototype of its own, creating a chain of prototypes, also known as the prototype chain. The prototype chain goes on and on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain

JavaScript uses this prototype chain to look for properties and methods of an object. When a property or method is accessed on an object, JavaScript first looks for it on the object itself. If it's not found, it looks for it on the object's prototype, and so on, until it reaches the end of the prototype chain. If the property or method is not found on any object in the prototype chain, it returns undefined.

An example of prototypical inheritance in JavaScript:

example.js

const animal = {
type: 'animal',
speak: function() {
console.log('Animals can speak');
}
};
const dog = Object.create(animal);
dog.name = 'Fido';
dog.speak();
// Output: "Animals can speak"

In this example, the animal object is a prototype for dog object. The dog object inherited the properties and methods of its prototype animal object. As we can see, the dog object does not have a speak method defined on it, but it can still call the speak() method from its prototype animal. Just like how you can still make dad jokes, even though you're not a dad.

In JavaScript, the Object.create() method is used to create a new object with a specified prototype, which can be an existing object or null.

It's possible to change (mutate) any property or method of an object that is in the prototype chain, or even replace the prototype of an object at runtime. This means that concepts like static dispatching, which is when the type of an object is determined at compile-time, do not exist in JavaScript.

Image by aj_aaaab on Unsplash
Static Dispatch vs. Dynamic Dispatch

In this blog post, we will discuss the differences between static and dynamic dispatch in programming. Static dispatch is a mechanism where the decision of which method or function to call is made at compile-time, based on the type of the object or variable passed as an argument. Dynamic dispatch, on the other hand, is a mechanism where the decision is made at runtime, based on the actual type of the object or variable. Both have their own advantages and limitations, and the choice of which approach to use depends on the specific requirements of the project.

#general

Strength or Weakness?

Prototype-based inheritance, which is the mechanism used in JavaScript, can be considered a weakness of the language in some situations.

One of the main issues with prototype-based inheritance is that it can make the code less predictable and harder to understand, especially for developers who are used to class-based inheritance. In a class-based system, it is clear where properties and methods are defined and how they are inherited, but in a prototype-based system, it can be less clear how properties and methods are inherited and how they can be overridden.

Another issue with prototype-based inheritance is that it can lead to unexpected behavior when modifying objects in the prototype chain. Because all objects that inherit from a prototype share the same properties and methods, modifying a property or method on the prototype can affect all objects that inherit from it, which can lead to bugs and unexpected behavior.

However, it's worth mentioning that prototype-based inheritance can also be considered a strength of the language. It is more dynamic and flexible than class-based inheritance, and it allows for more advanced patterns and techniques, such as dynamic delegation, mixins, and multiple inheritance.

It also allows for more flexibility in the code and it can be used to create more efficient data structures. Additionally, prototype-based object model can be a good fit for functional programming techniques.

You can find a table-summary of the pros and cons of prototypical-based inheritance below:

Pros Cons
More dynamic and flexible than class-based inheritance Can make the code less predictable and harder to understand
Allows for more advanced patterns and techniques, such as dynamic delegation, mixins, and multiple inheritance Can lead to unexpected behavior when modifying objects in the prototype chain
Allows for more flexibility in the code Can be a good fit for functional programming techniques
Can create more efficient data structures

In general, it depends on the use case and the developer's experience and preference. Some developers find prototype-based inheritance to be more powerful and flexible, while others find it to be less predictable and harder to understand.

Inheritance with the Prototype Chain: Dive Deeper

Property Inheritance

Developers can specify the prototype of an object by using the __proto__ syntax. Keep in mind that this is different from the obj.proto accessor, the former is the standard way to do it. Object literals, like { a: 1, b: 2, __proto__: c }, can also be used to specify the prototype of an object.

const obj = {
x: 5,
y: 6,
// __proto__ sets the [[Prototype]]. It's specified here
// as another object literal
__proto__: {
y: 7,
z: 8
}
};
// obj.[[Prototype]] has properties x and y
// obj.[[Prototype]].[[Prototype]] is Object.prototype
// Finally, obj.[[Prototype]].[[Prototype]].[[Prototype]] is null.
// This is the end of the prototype chain, as null, by definition,
// has no [[Prototype]].
// Thus, the full prototype chain looks like:
// { x: 5, y: 6 } ---> { y: 7, z: 8 } ---> Object.prototype ---> null
console.log(obj.x); // 5
console.log(obj.y); // 6
// Is there a 'y' own property on obj? Yes, and its value is 6.
// The prototype also has a 'y' property, but it's not visited.
// This is called Property Shadowing
console.log(obj.z); // 8
// Is there a 'z' own property on obj? No, check its prototype.
// Is there a 'z' own property on obj.[[Prototype]]? Yes, its value
// is 8.
console.log(obj.w); // undefined
// Is there a 'w' own property on obj? No, check its prototype.
// Is there a 'w' own property on obj.[[Prototype]]? No, check its
// prototype.
// obj.[[Prototype]].[[Prototype]] is Object.prototype and
// there is no 'w' property by default, check its prototype.
// obj.[[Prototype]].[[Prototype]].[[Prototype]] is null, stop
// searching. No property found, return undefined.

In this example, obj has own properties x and y, and it's prototype is another object that has properties y and z. We can see that when accessing the properties on the obj, it first looks for it on the object itself, if not found it will check it's prototype and so on, if it's not found on the prototype chain it will return undefined.

What is Property Shadowing?

Property shadowing is a phenomenon that occurs when an object has an own property with the same name as a property in its prototype. In such cases, when trying to access the property on the object, JavaScript will return the value of the own property and ignore the property with the same name in the prototype.

Property shadowing can be useful when you want to override the value of a property that is inherited from the prototype, but it can also make the code less predictable and harder to understand, especially when the property shadowing happens in a nested object.

It's worth noting that property shadowing is specific to JavaScript and it's not a common feature

Method Inheritance

In JavaScript, functions can be added to objects as properties, but they are not referred to as "methods" like in other class-based languages. When a function is added to an object, it works the same way as any other property, and it can also be overridden by properties with the same name in the prototype chain, this is called property shadowing.

When an inherited function is called, it will use the object that inherited it as the this keyword instead of the object where it was defined in the prototype.

const parent = {
num: 3,
add() {
return this.num + 1;
}
};
console.log(parent.add()); // 4
const child = {
__proto__: parent
};
console.log(child.add()); // 4
child.num = 5;
console.log(child.add()); // 6

In this example, parent is an object that has own properties num and add. add is a function that returns the value of num plus 1. child is an object that inherits from parent through the __proto__ property.

As we can see when calling the add method on parent and child, it returns the expected result, since the function add is being inherited by the child object, and the this keyword points to the object that called it.

When we change the value of num property on child object, it shadows the num property on parent and the next call to add method on child object returns the updated value.

Constructor Functions

In JavaScript, a constructor function is a special type of function that is used to create and initialize new objects. The function is invoked with the "new" keyword, which creates a new object and binds the "this" keyword to the new object. The constructor function can then add properties and methods to the new object. The constructor function's name is usually capitalized to indicate that it is a constructor. For example:

function Person(name, age) {
this.name = name;
this.age = age;
}
let person1 = new Person("John", 30);
console.log(person1.name); // "John"
console.log(person1.age); // 30

In this example, the Person constructor function is used to create a new object with the properties "name" and "age". The object is then stored in the variable "person1".

It is a common practice in JavaScript to define methods in the prototype, rather than in the constructor, for better performance and easier to read code. Here's an example of how methods can be defined on the prototype of a constructor function:

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
}
let person1 = new Person("John", 30);
let person2 = new Person("Jane", 25);
person1.sayHello(); // "Hello, my name is John"
person2.sayHello(); // "Hello, my name is Jane"

In this example, the Person constructor function is used to create new objects with the properties "name" and "age". The method "sayHello" is then defined on the prototype of the Person constructor function, so that it can be used by any objects created with the Person constructor. When the method is invoked on an object, it uses the "this" keyword to reference the object's "name" property.

By defining methods on the prototype, all objects created with the constructor will share the same function instance, rather than having their own copy of the function, which can save memory and improve performance.

This approach also allows you to add methods to all objects created with a constructor after they have been created, which can be useful if you want to extend the functionality of a class of objects without modifying the objects themselves.

Manipulate the prototype of a constructor function

Manipulating the prototype of a constructor function is a way to change the behavior of all instances created by that constructor. This can be done by adding or modifying properties and methods on the prototype object. For example, if we want all instances of a constructor function to have a new method, we can add that method to the prototype object.

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){console.log("Hello")}
let person1 = new Person("John", 30);
let person2 = new Person("Jane", 25);
person1.sayHello() // "Hello"
person2.sayHello() // "Hello"

In this example, the method sayHello is added to the prototype of the Person constructor function, so that it can be used by any objects created with the Person constructor. When the method is invoked on an object, it will work as expected because the object's internal [[Prototype]] property is pointing to the constructor function's prototype property which contains the method sayHello.

However, there are some weaknesses to keep in mind when manipulating the prototype of a constructor function:

  • It can lead to unexpected behavior if the prototype is modified after instances have already been created, as the instances will still have a reference to the old version of the prototype.

  • It can cause performance issues if the prototype is large or complex, as all instances must access the properties and methods on the prototype object.

  • Re-assigning Constructor.prototype (Constructor.prototype = ...) is a bad idea for two reasons: firstly, because it will cause the [[Prototype]] of instances created before the reassignment to reference a different object from the [[Prototype]] of instances created after the reassignment, which means that mutating one's [[Prototype]] no longer mutates the other. Secondly, unless you manually re-set the constructor property, the constructor function can no longer be traced from the instance.constructor, which may break user expectation.

It's worth noting that classes introduced in ECMAScript 6 provide a more elegant way of working with the prototype mechanism, and they have some features that help to overcome some of the weaknesses of constructor functions.

Different ways of creating and mutating prototype chains

1. Using the Object.create() method

Code example:

const proto = {greet: () => console.log("Hello")};
const obj = Object.create(proto);
obj.greet(); // Output: "Hello"
  • Features:

    • Allows for creating an object with a specific prototype without having to use a constructor function.

    • The resulting object inherits properties and methods from the specified prototype object.

    • Allows for creating an object with a null prototype, which is useful for creating an object with no inherited properties or methods.

  • Limitations:

    • The resulting object cannot have its own properties defined at the time of creation.

    • It's not possible to set the prototype of an existing object using this method.

  • When to use:

    • When you want to create an object with a specific prototype without using a constructor function.

    • When you want to create an object with no inherited properties or methods. When you want to create an object with a null prototype and not to inherit from the Object.prototype.

It's important to note that Object.create() method is a useful way to create an object with a specific prototype, and it's useful for creating objects that don't inherit from the Object.prototype, but it does not have the ability to set the prototype of an existing object or to define properties on the object at the time of creation.

2. Using the Object.setPrototypeOf() method

Code example:

const proto = {greet: () => console.log("Hello")};
const obj = {};
Object.setPrototypeOf(obj, proto);
obj.greet(); // Output: "Hello"
  • Features:

    • Allows for changing the prototype of an object after it has been created.

    • Can be used to set the prototype of any object, including built-in objects.

  • Limitations:

    • It's not possible to create a new object and set its prototype at the same time.

    • It's not available in older versions of JavaScript and has to be polyfilled.

  • When to use:

    • When you want to change the prototype of an existing object.

    • When you want to set the prototype of a built-in object and it's not available in older versions of JavaScript.

    • When you want to change the prototype of an object but don't want to change its properties or methods.

It's important to note that Object.setPrototypeOf() method is a useful way to change the prototype of an existing object, and it can be used to set the prototype of any object including built-in objects, but it does not allow to create a new object and set its prototype at the same time, and is not available in older versions of JavaScript so it has to be polyfilled.

3. Using the __proto__ property

Code example:

const proto = {greet: () => console.log("Hello")};
const obj = {};
obj.__proto__ = proto;
obj.greet(); // Output: "Hello"
  • Features:

    • Allows for access and setting the prototype of an object.

  • Limitations:

    • Not recommended to use as it's not part of the ECMAScript standard, it's deprecated and might not be supported in future versions of JavaScript.

  • When to use:

    • It's not recommended to use this property and it's better to use other methods such as Object.create(), Object.setPrototypeOf(), or classes as they are more reliable and well supported.

It's important to note that while the __proto__ property may be supported in some JavaScript environments, it's best practice to avoid using it and use the recommended methods instead, as it might not be supported in future versions of JavaScript and other JavaScript engines.

4. Using classes:

Code example:

class Person {
constructor(name){
this.name = name
}
greet(){console.log("Hello, my name is "+ this.name)}
}
const person1 = new Person("John");
person1.greet(); // Output: "Hello, my name is John"
  • Features:

    • Provides a more elegant way of working with the prototype mechanism.

    • Allows for creating constructor functions and defining methods and properties on the prototype object.

    • Makes it easy to trace the source of a property or method on an object.

    • Provides a way to implement inheritance, encapsulation and polymorphism.

  • Limitations:

    • Classes are not supported in older versions of JavaScript and has to be transpiled.

    • Classes are syntactic sugar over the underlying prototype mechanism, and may not be suitable for certain use cases.

  • When to use:

    • When you want to work with the prototype mechanism in a more elegant way.

    • When you want to implement inheritance, encapsulation and polymorphism.

    • When you are working with modern version of JavaScript and don't have to support older ones.

It's important to note that while classes are a more elegant way of working with the prototype mechanism and are well supported in modern versions of JavaScript, they may not be suitable for certain use cases and it's important to choose the right method depending on the environment and the requirements.

5. Using Object.defineProperty

This method allows to define properties of an object, including the prototype, with specific attributes like writable, enumerable, and configurable.

Code example:

const proto = {};
Object.defineProperty(proto,'greet', {
value: () => console.log("Hello"),
writable: true,
enumerable: true,
configurable: true
});
const obj = Object.create(proto);
obj.greet(); // Output: "Hello"
  • Features:

    • Allows for defining properties of an object, including the prototype, with specific attributes like writable, enumerable, and configurable.

    • Allows for defining getters and setters for a property.

    • Allows for controlling the behavior of a property, like if it can be enumerated or modified.

  • Limitations:

    • It can only be used to define properties one at a time.

    • It can only be used for properties, not for methods.

  • When to use:

    • When you want to define properties with specific attributes like writable, enumerable, and configurable.

    • When you want to define a getter or setter for a property.

    • When you want to control the behavior of a property.

    • When you want to define properties on an object but don't want to change its existing properties or methods.

Object.defineProperty() method is a useful way to define properties with specific attributes, it can be used to define properties one at a time and it can also be used to define getters and setters for a property, but it can only be used for properties, not for methods. It's also not suitable for defining a large number of properties at once.

Conclusion

It's important to note that all these methods allow to create and manipulate the prototype chain, but they have different features and limitations. It's important to choose the right method depending on the use case and the environment.

Performance

The performance of the lookup time for properties in JavaScript depends on the structure of the prototype chain. The longer the prototype chain, the longer it takes to look up a property. But don't worry, it's not like you're looking for a needle in a haystack, it's more like looking for a needle in a stack of needles!

When an object is asked for a property, JavaScript will first check if the property exists on the object itself. If it does not, it will then check the object's prototype, and continue to check the prototype's prototype, and so on, until it finds the property or reaches the end of the prototype chain. This process is known as the prototype chain resolution or the property lookup.

JavaScript engines use various optimization techniques to speed up this process, such as hidden classes and inline caching. These techniques allow the engine to quickly determine the prototype chain and the location of the property.In general, it's important to keep the prototype chain as short as possible to minimize the lookup time for properties. This can be achieved by using Object.create() method instead of new operator, not re-assigning the prototype property, and using classes with a single level of inheritance.

It's also important to note that the performance of property lookups is less of a concern in most cases, because the performance impact is usually small and most of the time it is not enough to affect the overall performance of an application.

Conclusion

In JavaScript, everything is either an object (instance) or a function (constructor) and "classes" are just constructor functions at runtime. Constructor functions have a special property called prototype which works with the new operator. The reference to the prototype object is copied to the internal [[Prototype]] property of the new instance. When you access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [[Prototype]] recursively, until it's found or Object.getPrototypeOf returns null. This means that all properties defined on prototype are effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances. It's essential to understand the prototypal inheritance model before writing complex code that makes use of it, also to be aware of the length of the prototype chains in your code and break them up if necessary to avoid possible performance problems. And the native prototypes should never be extended unless it is for the sake of compatibility with newer JavaScript features. Oh, and remember that prototype is not just a Clark Kent thing, it's also a JavaScript thing!

Tags:
front-end
javascript
My portrait picture

Written by Alissa Nguyen

Alissa Nguyen is a software engineer with main focus is on building better software with latest technologies and frameworks such as Remix, React, and TailwindCSS. She is currently working on some side projects, exploring her hobbies, and living with her two kitties.

Learn more about me


If you found this article helpful.

You will love these ones as well.

  • Photo by Esther Jiao on Unsplash
    May 04 2023 — 5 min readForms and Validations in HTML
    #front-end#html#security
  • Photo by Matt Hardy on Unsplash
    May 04 2023 — 5 min readTailwindCSS vs. Styled Components
    #css#front-end
  • Photo by Huy Phan on Unsplash
    May 04 2023 — 5 min readUnderstanding Fetch API and AJAX
    #general#javascript

  • Built and designed by Alissa Nguyen a.k.a Tam Nguyen.

    Copyright © 2024 All Rights Reserved.