📜 ⬆️ ⬇️

A quick note about inheritance in Node.js

In JavaScript, there are many different ways of inheritance, class and prototype, factory and through impurities, direct and indirect, as well as hybrids of several methods. But Node.js has its own native method using util.inherits (ChildClass, ParentClass). Until recently, I used the Nod method only for the built-in classes (when you need to make your successor for the EventEmitter, Readable / Writable Stream, Domain, Buffer, etc.), and for common domain modeling I used common practices for the entire JavaScript. And here, for the first time, it took to implement its own hierarchy of system classes, not inheritors from the built-in, but not the domain classes, but the classes massively affected in the system code of the Impress application server. And the simple use of util.inherits was somehow not enough, I looked for the article and didn’t find everything I needed, studied the examples of inheritance in the source code of the node itself, thought and made an example of the native node inheritance as a memory and wrote this small note, so that she, I hope, will also help you. Immediately I warn you that I don’t really like the implementation of the method call of the parent class from the method overridden in the child class because of the bulkiness, therefore, I welcome alternative ways and invite you to commit them to the repository or in the comments to this note.


Requirements for implementation:

Basic example
We have two classes connected by inheritance and call the parent class constructor from the child's constructor via ClassName.super_.apply (this, arguments). Naturally, this call can be either at the beginning of the child, the constructor, or its end or in the middle. The call can be wrapped in a condition, i.e. we fully manage rollback to ancestor constructor functionality.

var util = require('util'); //   function ParentClass(par1, par2) { this.parentField1 = par1; this.parentField2 = par2; } function ChildClass(par1, par2) { ChildClass.super_.apply(this, arguments); this.childField1 = par1; this.childField2 = par2; } //  util.inherits(ChildClass, ParentClass); //        var obj = new ChildClass('Hello', 'World'); console.dir({ obj: obj }); /* : { obj: { parentField1: 'Hello', parentField2: 'World', childField1: 'Hello', childField2: 'World' } } */ 

Advanced example
Here we define methods and properties both for the parent class and for the child class, through the prototype. Let me remind you that these will be methods and properties of not generated instances, but of the classes themselves, i.e. they will be visible in instances, but are contained in the prototypes. From the output to the console, you can see that everything works as it should, conveniently and predictably.
')
 var util = require('util'); //    function ParentClass(par1, par2) { this.parentField1 = par1; this.parentField2 = par2; } //    ParentClass.prototype.parentMethod = function(par) { console.log('parentMethod("' + par + '")'); }; //    ParentClass.prototype.parentField = 'Parent field value'; //    function ChildClass(par1, par2) { ChildClass.super_.apply(this, arguments); this.childField1 = par1; this.childField2 = par2; } //  util.inherits(ChildClass, ParentClass); //    ChildClass.prototype.childMethod = function(par) { console.log('childMethod("' + par + '")'); }; //    ChildClass.prototype.childField = 'Child field value'; //      var parentClassInstance = new ParentClass('Marcus', 'Aurelius'); var childClassInstance = new ChildClass('Yuriy', 'Gagarin'); //   console.dir({ parentClassInstance: parentClassInstance, childClassInstance: childClassInstance }); console.dir({ objectFieldDefinedInParent: childClassInstance.parentField1, classFieldDefinedInParent: childClassInstance.parentField, objectFieldDefinedInChild: childClassInstance.childField1, classFieldDefinedInChild: childClassInstance.childField }); parentClassInstance.parentMethod('Cartesius'); childClassInstance.childMethod('von Leibniz'); /* : { parentClassInstance: { parentField1: 'Marcus', parentField2: 'Aurelius' }, childClassInstance: { parentField1: 'Yuriy', parentField2: 'Gagarin', childField1: 'Yuriy', childField2: 'Gagarin' } } { objectFieldDefinedInParent: 'Yuriy', classFieldDefinedInParent: 'Parent field value', objectFieldDefinedInChild: 'Yuriy', classFieldDefinedInChild: 'Child field value' } parentMethod("Cartesius") childMethod("von Leibniz") */ 

An example with overriding methods
Further more interesting, ParentClass has a method methodName and we need to redefine it at the successor of ChildClass with the ability to call the ancestor method from the new redefined implementation.

 var util = require('util'); //    function ParentClass(par1, par2) { this.parentField1 = par1; this.parentField2 = par2; } //    ParentClass.prototype.methodName = function(par) { console.log('Parent method implementation: methodName("' + par + '")'); }; //    function ChildClass(par1, par2) { ChildClass.super_.apply(this, arguments); this.childField1 = par1; this.childField2 = par2; } //  util.inherits(ChildClass, ParentClass); //      ChildClass.prototype.methodName = function(par) { //     ChildClass.super_.prototype.methodName.apply(this, arguments); //   console.log('Child method implementation: methodName("' + par + '")'); }; //     var childClassInstance = new ChildClass('Lev', 'Nikolayevich'); //   childClassInstance.methodName('Tolstoy'); /* : Parent method implementation: methodName("Tolstoy") Child method implementation: methodName("Tolstoy") */ 
This construct for calling the parent class method is of course very cumbersome: ClassName.super_.prototype.methodName.apply (this, arguments) but I did not find another way for the native Nodal implementation of inheritance. The only dubious improvement that occurred to me was given in the following example.

Alternative inheritance
In order to simplify the syntax of the ancestor method call, we will have to pay with performance and adding the override method to the base class Function, i.e. for all functions in general (in the current context of the node, or inside the sandbox / sandbox, if all this happens inside the code running in a shielded memory context - the sandbox). The call then becomes elegant: this.inherited (...) or you can use the universal option: this.inherited.apply (this, arguments), in which you do not need to substitute all the parameters by name into the call to the parent method.

 var util = require('util'); //     Function.prototype.override = function(fn) { var superFunction = this; return function() { this.inherited = superFunction; return fn.apply(this, arguments); }; }; //    function ParentClass(par1, par2) { this.parentField1 = par1; this.parentField2 = par2; } //    ParentClass.prototype.methodName = function(par) { console.log('Parent method implementation: methodName("' + par + '")'); }; //    function ChildClass(par1, par2) { ChildClass.super_.apply(this, arguments); this.childField1 = par1; this.childField2 = par2; } //  util.inherits(ChildClass, ParentClass); //      ChildClass.prototype.methodName = ParentClass.prototype.methodName.override(function(par) { //     this.inherited(par); //  this.inherited.apply(this, arguments); //   console.log('Child method implementation: methodName("' + par + '")'); }); //     var childClassInstance = new ChildClass('Lev', 'Nikolayevich'); //   childClassInstance.methodName('Tolstoy'); /* : Parent method implementation: methodName("Tolstoy") Child method implementation: methodName("Tolstoy") */ 

UPD: Best Option with Overriding Methods
Together with xdenser , the fastest and most concise version was found that does not use super_ and does not require apply.

 var util = require('util'); //     function override(child, fn) { child.prototype[fn.name] = fn; fn.inherited = child.super_.prototype[fn.name]; } //    function ParentClass(par1, par2) { this.parentField1 = par1; this.parentField2 = par2; } //    ParentClass.prototype.methodName = function(par) { console.log('Parent method implementation: methodName("' + par + '")'); console.dir({t1:this}) this.parentField3 = par; }; //    function ChildClass(par1, par2) { ChildClass.super_.call(this, par1, par2); this.childField1 = par1; this.childField2 = par2; } //  util.inherits(ChildClass, ParentClass); //      override(ChildClass, function methodName(par) { //     methodName.inherited.call(this, par); //   console.log('Child method implementation: methodName("' + par + '")'); this.childField3 = par; }); //     var childClassInstance = new ChildClass('Lev', 'Nikolayevich'); //   childClassInstance.methodName('Tolstoy'); /* : Parent method implementation: methodName("Tolstoy") Child method implementation: methodName("Tolstoy") */ 

Performance comparison
The preferred option is obvious, but still need to be measured. Calling the ancestor class method on the same 10,000,000 call equipment:
Repository with code examples and comments in Russian and English: https://github.com/tshemsedinov/node-inheritance

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


All Articles