3

Understanding JavaScript Prototypes and Its Methods

 1 year ago
source link: https://hackernoon.com/understanding-javascript-prototypes-and-its-methods
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Understanding JavaScript Prototypes and Its Methods

The factory pattern is a well-known design pattern used in software engineering to abstract away the process of creating specific objects. The Constructor Pattern is used to create specific types of objects in JavaScript. It solves the problem of having duplicate functions in the global scope but also creates clutter in the code. We can solve this problem as following: using the constructor pattern instead of the factory pattern to create multiple objects with the same interface. The pattern 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.
image
Audio Presented by
Speed:
Read by:
Your browser does not support theaudio element.

Olga Kiba

Love Chinese culture and coding

Credibility

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 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.

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

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:

1. The relationships between the constructor, prototype, and instances

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

2. The instance’s property changes

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:

3. Prototype chain

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK