Published on Jan 19 2023
Last updated on Jan 20 2023
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.
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.
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
1const animal = {2type: 'animal',3speak: function() {4console.log('Animals can speak');5}6};78const dog = Object.create(animal);9dog.name = 'Fido';10dog.speak();11// 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.
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.
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.
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.
1const obj = {2x: 5,3y: 6,4// __proto__ sets the [[Prototype]]. It's specified here5// as another object literal6__proto__: {7y: 7,8z: 89}10};11// obj.[[Prototype]] has properties x and y12// obj.[[Prototype]].[[Prototype]] is Object.prototype13// Finally, obj.[[Prototype]].[[Prototype]].[[Prototype]] is null.14// This is the end of the prototype chain, as null, by definition,15// has no [[Prototype]].16// Thus, the full prototype chain looks like:17// { x: 5, y: 6 } ---> { y: 7, z: 8 } ---> Object.prototype ---> null1819console.log(obj.x); // 52021console.log(obj.y); // 622// Is there a 'y' own property on obj? Yes, and its value is 6.23// The prototype also has a 'y' property, but it's not visited.24// This is called Property Shadowing2526console.log(obj.z); // 827// Is there a 'z' own property on obj? No, check its prototype.28// Is there a 'z' own property on obj.[[Prototype]]? Yes, its value29// is 8.3031console.log(obj.w); // undefined32// Is there a 'w' own property on obj? No, check its prototype.33// Is there a 'w' own property on obj.[[Prototype]]? No, check its34// prototype.35// obj.[[Prototype]].[[Prototype]] is Object.prototype and36// there is no 'w' property by default, check its prototype.37// obj.[[Prototype]].[[Prototype]].[[Prototype]] is null, stop38// 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
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.
1const parent = {2num: 3,3add() {4return this.num + 1;5}6};7console.log(parent.add()); // 489const child = {10__proto__: parent11};12console.log(child.add()); // 41314child.num = 5;15console.log(child.add()); // 616
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.
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:
1function Person(name, age) {2this.name = name;3this.age = age;4}56let person1 = new Person("John", 30);7console.log(person1.name); // "John"8console.log(person1.age); // 309
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:
1function Person(name, age) {2this.name = name;3this.age = age;4}56Person.prototype.sayHello = function() {7console.log(`Hello, my name is ${this.name}`);8}910let person1 = new Person("John", 30);11let person2 = new Person("Jane", 25);1213person1.sayHello(); // "Hello, my name is John"14person2.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.
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.
1function Person(name, age) {2this.name = name;3this.age = age;4}5Person.prototype.sayHello = function(){console.log("Hello")}6let person1 = new Person("John", 30);7let person2 = new Person("Jane", 25);8person1.sayHello() // "Hello"9person2.sayHello() // "Hello"10
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.
Object.create()
methodCode example:
1const proto = {greet: () => console.log("Hello")};2const obj = Object.create(proto);3obj.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.
Object.setPrototypeOf()
methodCode example:
1const proto = {greet: () => console.log("Hello")};2const obj = {};3Object.setPrototypeOf(obj, proto);4obj.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.
__proto__
propertyCode example:
1const proto = {greet: () => console.log("Hello")};2const obj = {};3obj.__proto__ = proto;4obj.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.
Code example:
1class Person {2constructor(name){3this.name = name4}5greet(){console.log("Hello, my name is "+ this.name)}6}7const person1 = new Person("John");8person1.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.
This method allows to define properties of an object, including the prototype, with specific attributes like writable, enumerable, and configurable.
Code example:
1const proto = {};2Object.defineProperty(proto,'greet', {3value: () => console.log("Hello"),4writable: true,5enumerable: true,6configurable: true7});8const obj = Object.create(proto);9obj.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.
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.
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.
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!
Written by Alissa Nguyen
FollowAlissa 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.
Built and designed by Alissa Nguyen a.k.a Tam Nguyen.
Copyright © 2024 All Rights Reserved.