Before of all letās consider what an object is in JavaScript:
The object is an instance of a specific reference type.
Wait, but what is a reference type?
InĀ ECMAScript, reference types are structures used to group data and functionality together and are often incorrectly called classes. Reference types are also sometimes called object definitions because they describe the properties and methods that objects should have.
Letās remember how we create a custom object in JavaScript:
const person = new Object();
person.name = 'Olga';
person.age = 26
person.sayName = function() {
console.log(this.name)
}
This line of code declares the variable āpersonā and assigns it a value ā a new instance of the ObjectĀ reference type. To do this we used Object()
a constructor which creates a simple object with only the default properties and methods. After we added our custom properties and methods to it.
Also, there is a shorthand form of object definition which mostly in use now:
const person = {
name : 'Olga',
age : 26,
sayName: function() {
console.log(this.name)
}
};
Object creation
Using the Object constructor or an object literal is a convenient way to create single objects, but what about creating multiple objects with the same interface? This will entail a lot of code duplication. To solve this problem, developers began using a variation of the factory pattern.
The Factory Pattern
The factory pattern is a well-known design pattern used in software engineering to abstract away the process of creating specific objects.
let's look at the example of implementing this pattern in JavaScript:
function createAnimal(name, speed) {
const obj = new Object();
obj.name = name;
obj.speed = speed;
obj.getSpeedInfo = function() {
console.log(`I run ${this.speed}`);
};
return obj;
}
const turtle = createAnimal('Bruno', 'slow');
const rabbit = createAnimal('Roger', 'fast');
This approach solved the problem of creating multiple similar objects. But the factory pattern didnāt address the issue of object identification (what type of object an object is).
The Constructor Pattern
Constructors in ECMAScript are used to create specific types of objects. There are native constructors, such as Object and Array, which are available automatically in the execution environment at runtime.
Also, you can define custom constructors that have properties and methods for your type of object. The previous example can be rewritten using the constructor pattern next way:
function Animal(name, speed) {
this.name = name;
this.speed = speed;
this.getSpeedInfo = function(){
console.log(`I run ${this.speed}`);
};
}
const turtle = new Animal('Bruno', 'slow');
const rabbit = new Animal('Roger', 'fast');
console.log(turtle.getSpeedInfo === rabbit.getSpeedInfo); //false
As you can see this approach is extremely suboptimal in terms of memory allocation and power, because with such an entity description, you will receive in each instance not a reference to the getSpeedInfo()
method, which is stored in the memory of the constructor function, but you will copy this function to each new instance. We can solve this problem as follows:
function Animal(name, speed) {
this.name = name;
this.speed = speed;
this.getSpeedInfo = getSpeedInfo;
}
function getSpeedInfo() {
console.log(`I run ${this.speed}`);
}
const turtle = new Animal('Bruno', 'slow');
const rabbit = new Animal('Roger', 'fast');
console.log(turtle.getSpeedInfo === rabbit.getSpeedInfo); //true
As they get speed info property now contains just a pointer to a function, both turtle and rabbit end up sharing the getSpeedInfo()
a function that is deļ¬ned in the global scope. This solves the problemĀ of having duplicate functions but also creates some clutter in the global scope. Also with more methods, all ofĀ a sudden the customĀ reference type definition is no longer nicely grouped in the code.Ā These problems are addressed by using the prototype pattern.
The Prototype Pattern
Some functions are created with a prototype property, which is an object containing properties and methods that should be available to instances of a particular reference type. This object is a prototype for the object to be created once the constructor is called. The beneļ¬t of using the prototype is that all of its properties and methods are shared among object instances and not created for each instance. Instead of assigning object information in the constructor, they can be assigned directly to the prototype:
function Animal() {}
Animal.prototype = {
name: 'Bruno',
speed: 'fast',
getSpeedInfo: function () {
console.log(`I run ${this.speed}`);
}
}
const rabbit = new Animal();
const turtle = new Animal();
console.log(rabbit.getSpeedInfo === turtle.getSpeedInfo); //true
How Prototypes Work
When a function is created, its prototype property is also created. By default, all prototypes automatically get a property called constructor that points back to the function on which it is a property. In the previous example, the Animal.prototype.constructor points to Animal. Then other properties and methods may be added to the prototype:
When defining a custom constructor, the prototype gets the constructor property only by default (all other methods are inherited from the Object). Each time the constructor is called to create a new instance, that instance has an internal pointer to the constructorās prototype ā [[Prototype]]
. The important thing to understand is that a direct link exists between the instance and the constructorās prototype but not between the instance and the constructor.
function Animal() {}
Animal.prototype.name = 'Bruno';
Animal.prototype.speed = 'fast';
Animal.prototype.getSpeedInfo = function(){
console.log(`I run ${this.speed}`);
};
const turtle = new Animal();
const rabbit = new Animal();
console.log(turtle.hasOwnProperty('speed')); //false
turtle.speed = 'slow';
console.log(turtle.speed); //'slow' - from instance
console.log(turtle.hasOwnProperty('speed')); //true
console.log(rabbit.speed); //'fast' - from prototype
console.log(rabbit.hasOwnProperty('speed')); //false
delete turtle.speed;
console.log(turtle.speed); //'fast' - from the prototype
console.log(turtle.hasOwnProperty('speed')); //false
The parent of the child is called the prototype, which is where the name "prototype inheritance" comes from. Thus, there is a very economical consumption of memory:
But not every function has [[Prototype]]
. Only constructor functions have it.
Letās remember what is constructor functions. The only difference between constructor functions and other functions is how they are called. Any function that is called with the new operator acts as a constructor, whereas any function called without it acts just as you would expect a normal function call to act.
Arrow functions, functions defined by method syntax, asynchronous functions, built-in functions, and others do not havethem
.
proto
In 2012Ā Object.create
Ā appeared in the standard. Thanks to this, we were able to create objects with a given prototype but did have the ability to get or set it wherever we need. Some browsers implemented the non-standardĀ __proto__
Ā accessor that allowed developers to get/set a prototype at any time.
TheĀ __proto__
Ā Ā is not a property of an object, but an accessor property ofĀ Object.prototype
. In other words, it is a way to accessĀ [[Prototype]]
, it is notĀ [[Prototype]]
Ā itself.
If it is used as a getter, returns the object'sĀ [[Prototype]]
:
function Person (name) {
this.name = name;
}
Person.prototype.greeting = function () {
console.log('Hello');
};
let me = new Person('Olya');
console.log(me.__proto__ === Person.prototype); // true
We can write the prototype chain from the ā3. Prototype chainā image next way:
const arr = new Array();
console.log(arr.__proto__ === Array.prototype) //true
console.log(arr.__proto__.__proto__ === Object.prototype) //true
If you use __proto__
as a setter, returnsĀ undefined
:
function Person (name) {
this.name = name;
}
Person.prototype.greeting = function () {
console.log('Hello');
};
let me = new Person('Olya');
const MyOwnPrototype = {
greeting() {
console.log('Hey hey!');
},
};
me.__proto__ = MyOwnPrototype
console.log(me.__proto__ === Person.prototype); // false
Not every object in JavaScript has this accessor. Objects that do not inherit from Object.prototype
(for example, objects created as Object.create(null)
) donāt have it.
__proto__
Ā lets you set the prototype of an existing object, but generally, that's not a good idea. Letās see what the documentation says:
Changing theĀ
[[Prototype]]
Ā of an object is, by the nature of how modern JavaScript engines optimise property accesses, currently a very slow operation in every browser and JavaScript engine.
Object.prototype.__proto__
Ā is supported today in most browsers, but it is a legacy feature to ensure compatibility with web browsers. For better support, preferably useĀ Object.getPrototypeOf()
Ā andĀ Object.setPrototypeOf()
Ā instead.
In 2022, it was officially allowed to useĀ __proto__
Ā in object literalsĀ {...}
, but not as a getter/setterĀ obj.__proto__
.
It means that the only usage ofĀ __proto__
is as a property when creating a new object āĀ { __proto__: ... }
. But thereās a special method for this as well ā
Object.create(proto, [descriptors])
Ā ā creates an empty object with givenĀ proto
Ā asĀ [[Prototype]]
Ā and optional property descriptors:
const animal = {
speed: 'fast'
};
// create a new object with animal as a prototype
const rabbit = Object.create({__proto__: animal});
const turtle = Object.create(animal); // same as {__proto__: animal}
So why we shouldn't use __proto__
but should use Object.setPrototypeOf
if performance is the same?
__proto__
Ā itself is discouraged because it prevents an arbitrary object to be safely used as a dictionary:
let obj = {};
let key = '__proto__';
obj[key] = 'some value';
console.log(obj[key]); // [object Object], not 'some value'!
TheĀ __proto__
Ā property is special: it must be either an object orĀ null
. A string can not become a prototype. Thatās why an assignment of a string toĀ __proto__
Ā is ignored. So it is a bug.
Object.getPrototypeOf(), Object.setPrototypeOf()
In 2015,Ā Object.setPrototypeOf
Ā andĀ Object.getPrototypeOf
Ā were added to the standard, to perform the same functionality asĀ __proto__
and now they are recommended methods to get/set a prototype.
Object.getPrototypeOf(obj)
Ā ā returns theĀ [[Prototype]]
Ā ofĀ obj
.
Object.setPrototypeOf(obj, proto)
Ā ā sets theĀ [[Prototype]]
Ā ofĀ obj
Ā toĀ proto
.
Object.setPrototypeOf()
Ā is generally considered the proper way to set the prototype of an object. You should always use it in favor of the deprecatedĀObject.prototype.__proto__
Ā accessor.
Remember, it is not advisable to useĀ setPrototypeOf()
Ā instead ofĀ extends
Ā due to performance and readability reasons.
Letās use our new methods in practice:
function Person (name) {
this.name = name;
}
Person.prototype.greeting = function () {
console.log('Hello');
};
let me = new Person('Olya');
const MyOwnPrototype = {
greeting() {
console.log('Hey hey!');
},
};
Object.setPrototypeOf(me, MyOwnPrototype);
console.log(Object.getPrototypeOf(me) === Person.prototype); //false
console.log(Object.getPrototypeOf(me) === MyOwnPrototype); // true
Conclusion
You shouldnāt changeĀ [[Prototype]]
Ā existing objects. This is a bad practice both from the architecture side and from the speed side.
Of course, we can get/setĀ [[Prototype]]
Ā at any time but before it you should ask yourself: āIs it really necessary in this case or should I reconsider my architecture to not get into this situation next time?ā. A good practice is to set [[Prototype]]
once at the object creation time and donāt modify it anymore:Ā rabbit
Ā inherits fromĀ animal
, and it is what it is.
JavaScript engines support this ideology as much as possible. Subsequent prototype changing withĀ Object.setPrototypeOf
Ā orĀ obj.__proto__
Ā breaks internal optimizations for object property access operations and it will be very costly in terms of performance. So be careful with using those features unless you know what youāre doing.