This lesson will give a brief introduction to the topic of prototypes. It's impossible to say everything about it in one lesson. Not even a whole course is enough. Prototypes are learned only through practice combined with theoretical training. So don't worry if you don't understand everything the first time, it's normal.
Prototypes are the mechanism with the biggest impact on how objects work in JavaScript. They're rarely used directly in code (and usually only in libraries), but knowing them is important for understanding code behavior and debugging. Particularly when working with classes, which we'll cover later in the course. We'll merely go over the fundamentals in this lesson. We have another course listed in the supplementary materials, which will help you understand prototypes in depth.
In JavaScript, each object has a prototype associated with it. A prototype is a regular object stored in a special [[prototype]]
service field (this field cannot be directly accessed). It can be extracted like this:
const date = new Date();
// This function retrieves the object prototype from the object itself
const proto = Object.getPrototypeOf(date); // Date {}
// The prototype doesn't store the constructor
// What's stored there? Let's find out
proto === Date; // false
const numbers = [1, 2];
Object.getPrototypeOf(numbers); // [] – mapping is different, but it's an array
// Prototypes are also available for constructors, which we define ourselves
function Company(name) {
this.name = name;
}
const company = new Company();
Object.getPrototypeOf(company); // Company {}
What is it for? To address this topic, we must first understand how JavaScript property calls work. Until now, it was simple: if an object has a property, you can call it and get a value; if there is no property, you get undefined
when you access it. That's the truth, but not the whole truth. JavaScript is a little trickier and doesn't just look for a property in the object itself. If no property is defined for an object, JavaScript looks at the prototype and tries to figure out if the prototype has the necessary property. If there is, its value is returned.
In reality, the process is even more complicated. If the property is not found in the prototype, JavaScript looks at the prototype's prototype and so on until it reaches the end of the chain, and it does indeed have an end: the last prototype in the prototype chain is always null. Inheritance is implemented via this mechanism. This topic is beyond the scope of the current lesson.
Even normal JavaScript objects have a prototype:
Object.getPrototypeOf({}); // {} is an Object
This is the reason why even empty objects contain properties and methods (remember, properties and methods are in the prototype):
const obj = {}; // You can do the same thing like this: const obj = new Object();
// This is the constructor function that the current object was obtained from, in this case
obj.constructor; // [Function: Object]
// obj doesn't have its own constructor property, it came from a prototype
Object.hasOwn(obj, 'constructor'); // false
Object.hasOwn(obj, 'name'); // false
obj.name = 'hexlet';
// The name is in the object itself, because we just added it
Object.hasOwn(obj, 'name'); // true
You can access the prototype not only from objects, but also from the prototype
property of the constructor that creates those objects:
function Company(name) {
this.name = name;
}
// The same thing, obtained by different methods
// Company.prototype === Object.getPrototypeOf(new Company())
Now we can answer this question, “where does the prototype come from?” The prototype is the object in the prototype
property of the constructor function, not the constructor itself. It's easy enough to test the prototypes by modifying them.
// Add the getName property (make it a method)
Company.prototype.getName = function getName() {
// this still depends on the context in which it's called
return this.name;
}
const company = new Company('Hexlet');
// The property is available!
console.log(company.getName()); // => Hexlet
Nothing is stopping you from replacing the value of the getName
property in a specific object. This won't affect other objects in any way, since they retrieve getName
from the prototype:
const company1 = new Company('Hexlet');
const company2 = new Company('Google');
company2.getName = function getName() {
return 'Alphabet';
}
// This call will take the property from the object itself
company2.getName(); // Alphabet
// This call will take the value of the property from the prototype
company1.getName(); // Hexlet
Creating properties through a prototype is the proper way to create your abstractions in JavaScript. Any additional abstraction that we add to the code should resemble a constructor and prototype and have the appropriate features.
What is the benefit of this mechanism?
The simplest benefit is that extends the language kernel and libraries without direct access to the source code. Prototypes are an incredibly flexible mechanism that allows you to change anything from anywhere in the program (during runtime). For example, we can add or replace any methods in any objects of the language itself.
const numbers1 = [1, 3];
// As soon as this code is executed, all arrays,
// including those that have already been created, will have the last method
Array.prototype.last = function last() {
// This accessing is legitimate because it's a reference to the object itself,
// which in our case is an array
return this[this.length - 1];
}
numbers1.last(); // 3
const numbers2 = [10, 0, -2];
numbers2.last(); // -2
// Example of replacement
// a forbidden technique, never do that in real code
Array.prototype.map = function map() {
return 'Ehu!';
}
numbers1.map(); // "Ehu!"
Like any other powerful mechanism, prototypes are incredibly dangerous. You can make such a mess that programming becomes a living hell.
// Very evil code that breaks the push method
Array.prototype.push = function push(value) {
return this.unshift(value);
}
const numbers = [1, 2];
numbers.push(3);
console.log(numbers); // => [3, 1, 2] !!!
But if you use this mechanism wisely, you can get a lot of good things. For example, prototypes are actively used in testing, they help to spoof unwanted method calls that perform HTTP requests. Another popular option is polyfills. This is code that adds features from newer versions to older versions of JavaScrip. Not all of them, of course; only those that appear as properties and methods.
P.S. Now you know why in the documentation all function names are described like that: Number.prototype.toLocaleString()
.
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.