📜 ⬆️ ⬇️

Series 2. How to perform ancestor methods in prototype inheritance modification

image Let's design it in habrahabr.ru/blogs/javascript/130495, which is convenient for using the .inherit4 method of the Constr designer, to actually build a model of classes and inheritance (it will be more powerful than the classical one, but this is a side effect). If you do not want to connect Mootools with a similar model, this method of 2 KB of uncompressed code will be enough to work normally with prototype inheritance and have a couple of additional methods: access to ancestor methods. Using all 3 methods allows you to exclude the words prototype and constructor from the lexicon, while continuing to work with both, and makes the code easy to read.

In the 1st part on 2 functions, we built an implementation of an idea that can be used, but I would like to improve the quality of the future code. Wishlist for improvement:

1) to improve the writing and readability of the code when writing the inheritance tree - in particular, to write inheritance through the method of an object acting as a class (now it is a function);
2) use the execution of constructors to enable this.some_method = ... ; (now they are not executed, only prototypes work, therefore the traditional creation of methods is not available);
3) add a parameter for the expansion of the prototype of the constructor - we regularly, after each inheritance, expand the prototype of the descendant with new methods; it is necessary to put the operation on a regular basis; The previous article contains the extend () function;
4) embed .ancestor in the inheritance constructor (now we also have this function).

In fact, we want to get the Class object in terms of Mootools, but having access to the ancestors and performing regular actions to load the child prototype. And in general, the inheritance code is improving; this is one of the goals of building a method.
')
About access to the ancestors in the 1st part sounded rich in content critic, thanks to everyone, especially, lalaki , AndrewSumin . Its general direction is 1) access to the ancestors is not needed, except to the immediate ancestor, according to the basic principles of the PLO, otherwise it indicates poor-quality design of the model and the model must be changed. Also, 2) it is easy to access via (constructor-name prefix) .prototype.pred_ method.apply (this, parameters) , but also comments on OOP should not be ignored.

Answers:
1. Exactly, access to the distant ancestor speaks about the incorrect design (Liskov principle), except for the case when we want to insert a multiple inheritance node into the Procrustean inheritance bed JS - where 2 or more classes (constructors) are connected. And about access to the nearest parent, the principle says that it is permissible (how else can it be used). Our function makes it in the most abbreviated form, with the 2nd parameter by default (1) - .ancestor ('method').

2. Yes, access through 4 words " class.prototype.metho.apply " is, only 3 extra words are added instead of one: class name and 2 service words, and ' ancestor ' and the relative node number are sufficient. On the other hand, when rearranging the structure of inheritance, the verbose expression does not change, and the node number may change.

Thus, the criticism does not force us to turn away from the intended path - the purpose of introducing the .ancestor method was reasonable, we must complete the statement and present a fairly perfect way of inheritance containing our .ancestor . Nevertheless, criticism is important - it forces you not to forget that you need to avoid unnecessary parameter calls for executing methods, and ideally only referring to your immediate ancestor (for this, the second parameter in .ancestor defaults to 1) or not referring to ancestors at all.

Making the function method for Function


You can write the inherit () method (inheritance) through the base class prototype. We'll have to overload the base Function class, and this method will appear in every function. This is bad - each function is clogged, a source of conflicts with other software arises, but a very compact format for recording inheritance is created (this indicates a lack of constructor language for inheritance). Therefore, to illustrate and better understand the structure, we give an example of inheritance with this compact code.

Link to an example (Firebug is convenient to view the work and methods).

In fact, this is a test case with testing capabilities, like other links to examples in articles. Analysis and its construction will be lower, but no longer on the basis of Function , therefore, consideration of this code should be addressed if something is not clear further - or simply for aesthetic reasons. Moreover, this code and example do exactly the same as the following, more perfect.

Solution without overloading the base Function object


To avoid the appearance of methods in Function , create a class Constr (constructor) - an analogue of the Class constructor in Mootools. Immediately there is a need for additional movements - we can no longer write (constructor) .inherit , because the function does not have it. It is necessary to use either the prototype of the constructor (but this is long), or the Constr object to be considered a constructor, but in fact the constructor is a function in this object, or every time the constructor function is loaded with the definition of inherit () .

The latter, although somewhat costly (copying the link to each constructor), but more consistent with the concept of the constructor - it remains a function. Therefore, we’ll dwell on this approach: every time we will load (add) the definition of inherit () .

Inheritance in pictures


Here it is obvious that the multifunctional code is difficult to understand, therefore the scheme of how one step of inheritance works (using the .inherit4 () method) is given. Basically, it makes a new constructor an operation new and ensures that the new constructor has: 1) a prototype ( prototype ), 2) a function inherit4 () , and in a prototype that there are at least 4 properties: ancestor (function), extend (function ), constructor (a function is a link to itself), _anc (a function is a link to a parent constructor) and 3) to them are all the properties of the constructor and its prototype, and the properties of the prototype are more priority and do not require action. The first 2 properties of the prototype ( ancestor, extend ) are also copied automatically and do not require actions - there is no copying in the code.



Bottom line: recording inheritance functions


/** *        . spmbt, 2011 * @param {Constructor} Inherited - - ( extProto,   ) * @param {Object} extProto -     * @param contextArg0 - 1-     */ var Constr = function(){}; Constr.inherit4 = function(Inherited, extProto, contextArg0){ var f2 ={extend: function(obj, extObj){ //    if(obj ==null) obj = this; if(arguments.length >2) for(var a =1, aL = arguments.length; a < aL; a++) arguments.callee(obj, arguments[a]) else{ if(arguments.length ==1){ extObj = obj; obj = this; } for(var i in extObj) obj[i] = extObj[i]; } return obj; }, ancestor: function(name, level, constr){ //    level = level || (level ==0 ? 0 : 1); var t =this; return level <= 1 ? (level ? constr && constr.prototype[name] || t.constructor.prototype[name] : t[name]) : arguments.callee.call(this, name, level -1 , constr && (constr.prototype._anc != Function && constr.prototype._anc || constr.prototype.constructor) || t._anc ); }}; if(!this.prototype || !this.prototype.ancestor){ if(!this.prototype) this.prototype ={}; for(var i in f2) //       this.prototype[i] = f2[i]; } if(this === Constr && Inherited != Constr){ //   if(Inherited ===null) Inherited = Constr; return arguments.callee.call(Inherited, Inherited, extProto, contextArg0); }else{ if(Inherited || (Inherited && typeof Inherited !='function' && !extProto)){ //   ,   extend + ancestor     if(!extProto){ //  1 -       - extProto = typeof Inherited !='function' ? Inherited :{}; Inherited = typeof Inherited !='function' ? function(){} : Inherited; } Inherited.prototype = new this(contextArg0); Inherited.inherit4 = arguments.callee; f2.extend(Inherited.prototype, {_anc: this, constructor: Inherited}, extProto); return Inherited; }else{ if(this === window) return Constr; else{ this.prototype.constructor = this; return this; //  ,   - } } } }; //: A = Constr.inherit4(function(){this.prop ='A';}, {protoProp:'protoA'}); B = A.inherit4(function(){this.prop ='B';}, {protoProp:'protoB'}); C = B.inherit4(function(arg){this.prop ='C';this.propArg = arg ||'XX';}, {protoProp:'protoC'}); D = C.inherit4(function(arg){this.propArgD = arg ||'XX';}, {protoProp:'protoD'}, '3thArgInCInh'); var c01 = new D('ArgInD'); //B.prototype._anc = B; Alert(c01['protoProp'], c01.ancestor('protoProp', 0), c01.ancestor('prop', 0), c01.prop) //'protoD protoD DD' Alert(c01.constructor.prototype.protoProp, c01.ancestor('protoProp'), c01.ancestor('prop', 1)) //'protoD protoD C' Alert(c01.ancestor('protoProp', 2), c01.ancestor('prop', 2) ); //'protoC B' Alert(c01.ancestor('protoProp', 3), c01.ancestor('prop', 3) ); //'protoB A' Alert(c01.ancestor('protoProp', 4), c01.ancestor('prop', 4) ,'-- prop    ,    '); //'protoA C' Alert(c01.ancestor('protoProp', 5), c01.ancestor('prop', 5) ,'-- protoProp    '); //'protoA C' Alert(c01.ancestor('protoProp', 6), c01.ancestor('prop', 6) ); //'protoA C' Alert(c01.ancestor('protoProp2', 4), c01 instanceof A, c01 instanceof D, '--   - undedfined; instanceof - '); //'undefined D true true' Alert(c01.propArg, '--     C; ', c01.propArgD, '--    D'); 

(For a working example with a number of additional tests described below, see the console (output is in console.log ()).)

Terms of use (instead of documentation)


Creating root class:
 _ = Constr.inherit4(function(){_;}, __, 1___); //  ,    //  ,     - {}   _ 

Class inheritance:
 _ = _.inherit4(function(){_;}, ___, 1___); 

Creating an instance is, as usual, instance = new class () ;
The inheritance indicator is, as usual, an instance of the || class instanceof ancestor_class ;
// when instanceof multiple inheritance does not work, if you do not line up the inheritance tree

Appeal to ancestor method:
 .ancestor('', _); //   =1 

Possible reference to ancestor method by class name (not implemented in code, link # ):
 .ancestor(, '', [__]); 

Overloaded operations:
-----------------------
Adding one hash to yourself:
 __Constr.extend(xe); 

Adding a few hashes to yourself:
 .extend(null, xe, xe, ...); 

Expansion of any hash:
 _  = __Constr.extend(xe, xe, ...); //  


Intended Criticism and Responses


Analysis of testing will be a little further, and here - the answers for those who have already figured out.

1) here the class constructor is executed instead of new F () in the previous function; The environment will be loaded a little more if the application constructor is empty and is no different from F () . But it is possible to construct properties. Will the environment be loaded more when a property appears in the constructor? No, because otherwise they would be declared in the prototype, and their preparation would not be carried out in a constructor, but somewhere close by, which threatens with unstructured code. Therefore, it is better to do in the designer the formation of those properties of the prototype of the heir that we need and which depend on the designer.

2) Is the inheritance method becoming a bit heavier with a couple of additional functions and some checks? This is a prototype, so the declaration is weighted down once in the root constructor itself, but execution is not, if it is not used. If used, it should still be defined somewhere.
Checks, of course, slightly slow down the work, but we get a number of amenities. Do they need - to decide the developer. In my opinion, something that facilitates development is justified at least during the development phase.

3) Why is there only one contextArg0 argument for the ancestor? Because an attempt to write the construction of an object as Inherited.prototype = new (this.apply (this, contextArgs)); - does not work. But one argument will be enough to specify a hash with all the parameters in it - almost the same and more convenient description of the arguments.

4) Why an extra if entity (this === window) ... ? For an overloaded hash expansion method (with empty 1st argument or with 1 argument). Inheritance is not used.

Using Constructor Arguments


Constructor arguments are a very important and useful mechanism for specifying heirs or generated objects, which can determine the properties of a constructor and, thus, they are included in a full circle, becoming dynamic. But in the implementation it was not possible to pass many arguments in the array in order to scatter them through apply in the function new this () - this is not implemented in javascript. But the argument of the constructor is so important that we enter at least one - in place of the third argument in inherti4 . (It would be better if it were the first one, but then writing is complicated when the method is overloaded.)

There is a demonstration of use in the example. In class C, we determined the function that takes the first argument — we write the first argument for it in place of the third argument in C.inherit4 () . In class D, we identified a similar function — in the generation of the instance, the argument is used in its first place.

Of course, it is inconvenient that the argument jumps from place to place. But how to make the overload differently, given that the argument for the constructor is most often not? You can use that the current first argument is always a function, but then the argument for the constructor can also be a function. Therefore, let everything stay in place for the time being, but with real use of the mechanism, maybe we will invent something better. The main thing is that this mechanism now exists and works.

By the argument of the constructor, the function of automatically spreading the hash on the properties of this :

 function(arg){ //   if( typeof arg =='object') //    for(var i in arg) this[i] = arg[i]; } 

How to make this scattering automatic? To insert even a single call of something into the constructor function every time is not very interesting. Let's invent instead of simple construction of the successor

 Inherited.prototype = new this(contextArg0); 

extended:

 if(typeof contextArg0 =='object'){ Inherited.prototype = contextArg0; f2.extend(Inherited.prototype, new this(contextArg0)); }else Inherited.prototype = new this(contextArg0); 

As you can see, it works, but introduces an unobvious feature for constructing an object and adds another check. We will not include it in the final code, but we should try to apply it in order to display a convenient format.

If you swap the order of assignment,

  Inherited.prototype = new this(contextArg0); f2.extend(Inherited.prototype, contextArg0); 

we will get an atypical priority of assigning constructor properties (more priority than a prototype), which may come in handy sometime (but this is even more dangerous experiment on habits, therefore we also only mention it).

Analysis of tests and mechanism of action


You can compare the type of tests with an example from the first article . Almost the same thing is done (plus the execution of constructors), but the code is no longer so loose, the arguments fell into place. This is a consequence of the fouling of the function code with new checks and movement towards some goal.

The example began to involve the properties of constructors - those that arise from " this.xxx ". In the test example, we carefully arranged the letters A, B, C, D each in its own level. In the results we see the shift of letters. This is quite logical, because .ancestor extracts prototypes (except c01.ancestor ('prop', 0) ), and the prototype arises from the heir: the ancestor's property is written into the prototype. It turns out that we see next: the property - from the previous class, the prototype - from the current one. Only the instance of c01 in the sample prototype is not (not written), so it is taken from the previous class, so the first line - “protoD D” - is the same for the output letters, and in the following there is a shift.

In order for the properties from prototypes and constructors to behave in exactly the same way, it was necessary to catch the if condition (this === Constr) ... in order to detect the first ancestor and "loop" inheritance. Works, as in the first part of the story, the “bomb of kindness”, which does not give an error when addressing the missing property. However, it was ensured that the behavior when accessing the “too early” ancestor is the same for all properties: the first existing property of the parent is returned or undefined .

Tests that work in the example by reference show how multiple inheritance works (defective), how overloaded methods work in objects that have an ancestor Constr . About this - just below.

Inheritance model and its variations


Such a somewhat bizarre model of inheritance turned out, if both the properties of the constructors and the prototypes are involved. The model is more powerful than the classical inheritance, and if you look closely, then within it (this algorithm of 30 lines) you can implement an analog of the classical inheritance in more than two ways - only through prototypes or only through properties. Or inheritance "with a doubled frequency of steps" in this order:
  1. The base class is the prototype of constructor A (its properties are stored in the same place in the prototype);
  2. 1st heir - properties of constructor A (stored in the prototype of the 2nd heir);
  3. The 2nd heir is the prototype of constructor B (the properties are here, in the prototype of the 2nd heir);
  4. 3rd heir - properties of constructor B (stored in the prototype of the 4th heir);
  5. ...
Of course, this is not pure classical inheritance, because you can change the prototypes of any constructor, therefore, instantly change the properties of all heirs. On the other hand, the properties of ancestors are not stored in each class (unless there is optimization in the script engine), and if you avoid the “errors” of changing prototypes after creating successors, you will get classical inheritance, even in the “double frequency steps” mode.

Multiple inheritance


Compatibility, we must also say that multiple inheritance is not supported by the instanceof operation. Making it react to the tree of ancestors can only be a way to line up the tree in the line of ancestors. At the same time, due to a special feint in inherit4 () with the addition of the properties of the prototype, if it already exists, the merging of the branches from the ancestors is done normally. The code with mixing is not included in the example, so inheritance will not work - the prototype will receive the value only from the last ancestor.
 1 = Constr.inherit4(function(){this.prop ='prop1'; this.propA ='propA';}, {protoProp:'prot1'}); 2 = Constr.inherit4(function(){this.prop ='prop2';});  = 1.inherit4({protoProp:'prot11', protoPropA:'protA'});  = 2.inherit4(, {protoProp:'prot2'}); 

The correct result is the alignment of the merge tree into the " Ancestor1-Ancestor2-Heir " chain of inheritance - a case when the method of addressing ancestral methods with a generation greater than 1 comes in handy.

Where can I apply a double inheritance rate?


Where the structure of inheritance is determined and for each level we know what to write. For example, there is the task of using the default program settings (base class), which are overlapped by the “recommended settings”, which are overlapped by the “recommended priority”, which finally are overlapped by the user settings. Total - 4 levels of settings, everywhere - inheritance relations. Obviously, in JS they can be implemented by 2 constructors and inheritance of the described type.

In this example, even the class procedures will be the same. In cases more complicated - it is possible to write your own procedures for each “half-step” of inheritance.

All this is not a virtue, but simply an additional opportunity that you can not use, but write one step of inheritance per operation. Inherit4 () . And, of course, you need to understand the model of inheritance in JS for this use; knowledge of classical inheritance will not be enough.

Expansion of properties - we will extend the description of extend into “public”, and a number of other


Properties that are not necessary, because they solve side problems, but the code for them is written and used. This is an extend method for expanding hashes and the associated overloading of arguments — for use in different modes for different purposes. (These properties are already implemented in the function above and in the example.) Why? Yes, at least for faster merging hashes than in jQuery, where more type checks are done.
1) In fact, why should the good (in a few lines of the extend function) disappear, since it is written and ready to infiltrate wherever the Constr is defined? (You may not like the fact that it inserts extend into all inherited objects, but there are no problems to disable it.)

2) ... And, oh, walk - so walk, add to extend the possibility of expanding yourself, if you write one argument or in place of the first argument is null .

3) Additional convenience - we can write (constructor). Inherit4 () , without arguments, to include in the prototype both functions - extend and ancestor , if we are not going to inherit this class (constructor). (object) .ancestor ('method_name') will also make sense if the generated object ( object = new constructor (); ) was later attributed to a direct wiping method — the constructor’s prototype method (not the constructor itself) will have “human-readable” access (along with object.constructor.prototype [name] ).

Example (so it will be clearer than the same words):
Note that a similar example for Function.prototype.inherit3 also knows how, but is written differently.

  = Constr.inherit4(null, {a: 333}); //   = new (); //   - .a = 555; //  Alert(.a, .ancestor('a'), "--     "); //    : 333      : Alert(.extend({a:3}, {b:4}, {c:5}), "--      " ); //Object {a=3, b=4, c=5} 

4) Given that we are returning the first argument, it is possible to create a nameless heir, in order to assign it a name later (more correctly, assign it a name :)).

 //     (  - null) obj = new (Constr.inherit4()); Alert( obj.extend(null, {a:1}, {b:2}) ); //Object { a=1, b=2, _anc=function(), ... 

5) In the examples we have already forgotten that in the first part of the article we had to write explicitly the prototype of the object - now this is done by the 2nd parameter, it is visual and convenient:

 A = Constr.inherit4(function(){_ }, {_}); 

In this format:
 A = Function.inherit4(function(){this.prop ='A';}, {protoProp:'protoA'}); 
the code looks much better than with the scattered definition of properties.
As a result, we see that the code has grown "meat", but each section operates very effectively.

We should not forget that the .extend method was not attributed to the base Object class, so this is {x: 2} .extend ({a: 1}); - will not work (the cost of abstaining from expanding the base class). But

 Alert( (new (Constr.inherit4(function(){this.x = 2;}) ) ).extend({a:1}); 

- will be (gives the object {a: 1, x: 2, and a pair of functions} ). (A nightmare, of course, no one will do, but the idea is shown ( extend itself) and will work in the case of more stretched codes.) The main thing is that we do not lose anything (only the namespace is clogged ), the extend function has already been, just assigned to the prototype of the root object Constr , like ancestor .

PS If someone gives a link to the description of at least a very similar approach to inheritance or the same, we will all be very grateful - it’s interesting to know where this approach leads.

PS2 Do I need to get rid of the bomb of kindness and how?

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


All Articles