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)
}
};
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 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).
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 defined 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.
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 benefit 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
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
.
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.
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 deprecatedObject.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
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.