📜 ⬆️ ⬇️

An attempt to classify and analyze existing approaches to inheritance in Javascript


Some time ago, I got around to the topic, which has long been unnerving for me. This topic is inheritance in javascript.

There are many articles on this topic on the web, but I did not manage to find a synthesis analysis that would satisfy me with its completeness and logic. Why did you want to find a synthesis analysis? The fact is that the special, I would say, unique complexity of object-oriented programming in JS consists in the shocking (in any case, me) diversity of its possible implementations. After a rather lengthy unsuccessful search, I decided to try to figure this out on my own.

I want to say at once that I do not pretend to have a deep understanding of OOP in JavaScript, and I do not even pretend to have a deep understanding of OOP at all. I will be glad if my analysis attempt will be useful to someone, but the main purpose of the publication is, in a sense, the opposite - I would like to use the comments of people who are better oriented to the topic to clarify it for myself.

General reasoning


There is an opinion that JS is a very powerful and flexible language. On the other hand, it is believed that JS - well, let's say softly, the language is unfinished. Well, when it comes to OOP, JavaScript shows everything that it is capable of - on both points.
')
From this paradox, in particular, it follows that each javascript has a motive and it is possible to invent its own bicycle with a variable geometry of the wing approach to the inheritance of objects in JS. And, actually, the question that interested me personally: how to structure this diversity.

At first, I calculated that it would be possible to divide all the decisions into two large groups: class-based and prototype (“classless”). Such a division seemed natural, given the fact that JavaScript itself seemed to be “stuck” between the classical OOP style and the prototype one. However, I decided to refuse such a classification, due to the fact that in the overwhelming majority of cases, OOP in JavaScript is exactly the reproduction of a classic OOP by means of a prototype language.

Questions


Instead, I decided to use the following three questions, which seem to help “focus the eyes” on inheritance in JS:

1) How classes are implemented
2) How do instances get properties of their class?
3) How class inheritance is implemented

(Note: hereinafter, by properties, I understand both the actual properties and the methods of the objects - in fact, in prototype programming there is a suitable term “slot”, but it is rarely used in this context in JS)

How do we organize classes?


There are two fundamentally different answers to this question:

  1. Classes are organized using constructor functions.
    At the same time, instance objects are created using the new operator, and the designers themselves, in the JS style, combine several roles at the same time — simultaneously functions, and class, and object constructor.
  2. Classes are organized using object factory functions.
    In this case, instances are directly returned by factory functions.

The simplest example of class organization using a constructor might look like this:

//  1 -  (   ) function Class() { this.value = 'some value'; //  this.some_method = function() { //  console.log('some_method invoked'); } } var obj1 = new Class(); //   var obj2 = new Class(); //    

And here is a similar class organized with the help of a factory of objects:

  //  2 -  (   ) function Class() { var obj = {}; obj.value = 'some value'; //  obj.some_method = function () { //  console.log('some_method invoked'); } return obj; } var obj1 = Class(); //  ( ,       new) var obj2 = Class(); //   

(You may notice that the presence of the special keyword new hints that it is the constructor functions that are the mainstream language)

How do we store properties?


We turn to the second question: how do instances get the properties of their class?

Here, first of all, it is necessary to clarify what is meant by “obtaining class properties”. We are accustomed to thinking that objects automatically “possess” the properties and methods defined in their class. The fact of this "possession" we perceive as something given. But in JS it is not. Here we can choose how our objects will receive properties from their (pseudo) classes. We again have two possibilities: either objects receive properties of their class in the prototype, or they contain them directly.

As for the first option, we just observed it - the two previous examples implement direct storage of properties in an instance for constructor functions (example 1) and for factories of objects (example 2).

So what is the acquisition of properties from the prototype? That's how:

  //  3 - -,    function Class() { } Class.prototype.value = 'some value'; //  Class.prototype.some_method = function() { //  console.log('some_method invoked'); } var obj1 = new Class(); //   var obj2 = new Class(); //   

Please note that the constructor function itself is completely empty in our example, and all properties and methods are specified in the prototype. Of course, in reality, the constructor will most likely be used for something useful (for example, to set the initial parameters).

As you can see, prototypes are combined quite naturally with designers. And what about the factories? The situation with them is more complicated, since, according to the standard, the prototype of an object is specified using its constructor function. In other words, we cannot work with the prototype of an object unless we use constructors. So we have to use the trick:

  //  4 - ,    //     , //    ,     . function derivate(o) { function F() {} F.prototype = o; return new F(); } function Class() { return derivate(Class.obj); } //  ,         Class.obj = {}; Class.obj.value = 'some value'; //  Class.obj.some_method = function () { //  console.log('some_method invoked'); } var obj1 = Class(); //  var obj2 = Class(); //   

This example is somewhat more complicated than the previous ones. Is there any sense in this difficulty? Well, at a minimum, the example is interesting because the derivate () function expands exactly the prototypical capabilities of the language (well, or compensates for the lack of these capabilities). On the other hand, it is interesting that the code responsible for creating the class turned out to be very similar to the similar code from the previous example. In fact, instead of the built-in Class.prototype, we created our own Class.obj property, while we had to do something of what JavaScript provided us in the previous example. I want to note that with all its similarities, two examples are completely different in essence.

Why do we need to put the properties of the class in the prototype of the instance? At a minimum, this way we can save resources, since the properties of the same name of all instances of a class will physically occupy the same place in memory.

Constructors. How to inherit?


Finally, we come to the most interesting. The third question is inheritance.

Fundamentally, in a prototype language, the inheritance of properties can be implemented in one of two ways: by copying or delegating.

JavaScript itself implements delegation inheritance - in short, this means that the missing property of an object is searched for among the properties of its prototype.

Copy inheritance follows the concept of prototype concatenation . In this case, the properties from the parent object are simply copied to the child object. Prototype concatenation is not part of JavaScript, but does not mean that it cannot be implemented.

Solution with call / apply

Let's see how copying inheritance can be implemented. Theoretically, for this one could create an auxiliary function copyMethods:

  //  ,       function copyMethods(from, to) { for(m in from) { //       if (typeof from[m] != "function") continue; //  ,     to[m] = from[m]; //   } } 

However, it can be made much easier if you use a small focus with the apply method.

  //  5 -  +    +   //    //       function Class(val) { //        this.value = val; this.getName = function() { return "Class(id = " + this.getId() + ")"; } this.getId = function() { return 1; } } function SubClass() { //    ,     : // copyMethods(new Class(arguments[0]), this); //    : Class.apply(this, arguments); //  Class()   this      var super_getName = this.getName; //     ,    this.getName = function() { return "SubClass(id = " + this.getId() + ") extends " + super_getName.call(this); } this.getId = function() { return 2; } } //  o = new SubClass(5); console.log(o.value); // 5 console.log(o.getName()); // "SubClass(id = 2) extends Class (id = 2)" console.log(o instanceof Class); // false [   instanceof] 

This example is the first one to answer all three proposed questions, and in fact it is a ready-made solution, so I made it slightly less schematic than the previous ones. He completes what he started in Example 1.

Given the completeness of this solution, it makes sense to evaluate its pros and cons. So,

Than this is good:

- concise and simple implementation
- there is no need to repeat class names with each method definition ( DRY principle )
- automatic inheritance of constructors
- does not affect the global context of the program (no global support functions, etc.)
- easy to implement private properties

What is bad:

- memory is not effectively used (all identical properties of all objects are stored as copies)
- it is not convenient to call parent methods
- incompatible with instanceof operator
- not compatible with native classes (you cannot create a descendant of a built-in class like Date)

Almost standard approach

Of course, example 1 (constructors + class properties inside an instance) can be developed in a different direction, in accordance with the second answer option to the question about the mode of inheritance.

  //  6 -  +    +   //    //       function Class(val) { //        this.value = val; this.getName = function() { return "Class(id = " + this.getId() + ")"; } this.getId = function() { return 1; } } function SubClass() { var super_getName = this.getName; //    this.getName = function() { return "SubClass(id = " + this.getId() + ") extends " + super_getName.call(this); } this.getId = function() { return 2; } } SubClass.prototype = new Class(); //     //  o = new SubClass(5); console.log(o.value); // undefined [  ] console.log(o.getName()); // "SubClass(id = 2) extends Class (id = 2)" console.log(o instanceof Class); // true [  instanceof] 

Note that the child class does not know how to set the initial parameters. Are there any advantages to this approach compared to the previous one? Well, inheritance through prototypes is more energy efficient. On the other hand, this solution is obviously halfway - we still create class properties for each instance independently of each other.

Very standard approach

Finally, we come to a solution that probably was conceived as standard for JavaScript. From the point of view of our three questions, its “formula” looks like this: constructors + class properties in the prototype + inheritance by delegation.

  //  7 -  +    +   //    //       function Class(val) { this.init(val); } Class.prototype.init = function (val) { this.value = val; } Class.prototype.value = 5; //    Class.prototype.getName = function() { return "Class(id = " + this.getId() + ")"; } Class.prototype.getId = function() { return 1; } function SubClass(val) { this.init(val); } SubClass.prototype = new Class(); //   SubClass.prototype.class_getName = SubClass.prototype.getName; //    SubClass.prototype.getName = function() { return "SubClass(id = " + SubClass.prototype.getId() + ") extends " + SubClass.prototype.class_getName(); } SubClass.prototype.getId = function() { return 2; } function SubSubClass(val) { this.init(val); } SubSubClass.prototype = new SubClass(); SubSubClass.prototype.subclass_getName = SubSubClass.prototype.getName; //    SubClass.prototype.getName = function() { return "SubSubClass(id = " + SubSubClass.prototype.getId() + ") extends " + SubSubClass.prototype.subclass_getName(); } //  o = new SubSubClass(5); console.log(o.value); // 5 console.log(o.getName()); // "SubSubClass(id = 2) extends SubClass(id = 2) extends Class(id = 2))" console.log(o instanceof Class); // true [  instanceof] 

I so wanted to emphasize the inconvenience of this example, that I described in it three whole classes. It seems that the whole example consists of repeating the same names, the same verbose constructs. But this is not enough, the very method of inheriting a prototype contains fundamental inefficiency: in order to describe the hierarchy we must create and initialize (that is, call the init method) prototype objects. This is bad, if only because the initialization can be resource-intensive.

Oddly enough, despite the shortcomings, this approach is very popular. Why? In fact, there is no riddle here: the shortcomings of the method are compensated with the help of all kinds of add-ins, but its “nativeness”, embeddedness in the syntax of the language, cannot be guaranteed with any add-on.

Well, we still have the last option from the four solutions with constructors: constructors + properties in prototypes + inheritance by copying. I will leave it as an independent exercise for those who want to experience what it means to program against the wind;) I can only say that theoretically this approach is possible, and even sometimes used for grinding (in any case, I have met it online), but it is extremely inconvenient (using with call / apply is impossible, and inheriting properties by copying after you saved them in the prototype is simply not logical)

Thus, we have finished with designers, about factories of objects - in continuation.

Update: unfortunately, after more than a year after writing the first part of the article, I have to admit that there is no time for the second part - and, apparently, it won't be. I apologize to all interested parties :)

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


All Articles