📜 ⬆️ ⬇️

Prototypes are objects (and why this is important)

JavaScript is one of the main languages ​​of our stack in Hexlet. We use ReactJS and NodeJS in the interactive parts of the platform, and have done an introductory course (more advanced ones are on the way). Love for JS helped publish this translation of a good essay "Prototypes are Objects (and why that matters)".

This post is designed for those who are familiar with objects in JavaScript and know how a prototype determines the behavior of an object, what a constructor function is and how the .property property of the constructor refers to the object that it constructs. A general understanding of the syntax of ECMAScript 2015 does not hurt either.

We could always create a class in JavaScript like this:
')
function Person (first, last) { this.rename(first, last); } Person.prototype.fullName = function fullName () { return this.firstName + " " + this.lastName; }; Person.prototype.rename = function rename (first, last) { this.firstName = first; this.lastName = last; return this; } 


Person is a constructor function, as well as a class in the JavaScript sense of the word. ECMAScript 2015 allows you to use the keyword class and the so-called. “Compact method notation”. This is syntactic sugar for writing functions and assigning methods to its prototype (everything is a little more complicated there, but now it doesn’t matter). So we can write a Person class like this:

 class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; 


Cool. But under the hood, there is still a constructor function with a binding to the name of Person , and there is an Person.prototype object that looks like this:

 { fullName: function fullName () { return this.firstName + " " + this.lastName; }, rename: function rename (first, last) { this.firstName = first; this.lastName = last; return this; } } 


Prototypes are objects

If you need to change the behavior of an object in JavaScript, you can add, remove, or change the methods of an object by adding, deleting, or changing functions associated with the properties of this object. This is in contrast to many “ class ical” languages ​​in which there is a special form (for example, Ruby has def) for defining methods.

JavaScript prototypes are “just objects”, and thanks to this we can add, delete, or change prototype methods by adding, removing, or changing functions associated with the properties of this prototype.

This is exactly what ECMAScript 5 code does above, and the syntax class “devours” it into equivalent code.

Prototypes are “only objects”, and this means that we can use any techniques that work on objects. For example, instead of binding a single function to a prototype, we can perform a bulk binding using Object.assign :

 function Person (first, last) { this.rename(first, last); } Object.assign(Person.prototype, { fullName: function fullName () { return this.firstName + " " + this.lastName; }, rename: function rename (first, last) { this.firstName = first; this.lastName = last; return this; } }) 


And, of course, we can use compact syntax if we want:

 function Person (first, last) { this.rename(first, last); } Object.assign(Person.prototype, { fullName () { return this.firstName + " " + this.lastName; }, rename (first, last) { this.firstName = first; this.lastName = last; return this; } }) 


Mixins (impurities)

Since the class “devours” the code into constructor functions and prototypes, we can use impurities like this:

 class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; Object.assign(Person.prototype, { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } }) 


We have just “involved” methods for collecting books into the Person class. It's cool that you can just write code like this, but you can also give names:

 const BookCollector = { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } }; class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; Object.assign(Person.prototype, BookCollector); 


So you can continue as you like:

 const BookCollector = { addToCollection (name) { this.collection().push(name); return this; }, collection () { return this._collected_books || (this._collected_books = []); } }; const Author = { writeBook (name) { this.books().push(name); return this; }, books () { return this._books_written || (this._books_written = []); } }; class Person { constructor (first, last) { this.rename(first, last); } fullName () { return this.firstName + " " + this.lastName; } rename (first, last) { this.firstName = first; this.lastName = last; return this; } }; Object.assign(Person.prototype, BookCollector, Author); 


Why use impurities

Building classes with basic functionality (Person) and mixins (BookCollector and Author) provides some advantages. First, sometimes the functionality cannot be well decomposed into parts in a beautiful tree structure. Book authors can be corporations, not people. And antique bookstores collect books just like book lovers.

Impurities such as BookCollector or Author can be interfered with in several different classes. Attempts to composition functionality using inheritance are not always successful.

Another advantage is not so obvious in a simple example, but in production systems, classes can grow to ridiculous sizes. Even if the impurity is not used in several classes, decomposition of a large class with the help of impurities helps to satisfy the principle of the principle of a single duty . Each mixin can only have one area of ​​responsibility. All this simplifies understanding and testing.
why is it important

There are other ways of decomposing responsibilities in classes (for example, delegation and composition), but the point is that if you decide to use mixins, then this is a very simple way, because JavaScript doesn't have a large and complex OOP mechanism that would drive you into clear framework.

For example, in Ruby it is easy to use mixins, because from the very beginning there is a special feature - modules. In other OO languages, mixins are difficult to use because the class system does not support them, and they do not really fit in with meta-programming.

Source: https://habr.com/ru/post/260427/


All Articles