📜 ⬆️ ⬇️

A fresh look at impurities in javascript

In this article, I will explore impurities in detail in JavaScript, and show a less common, but, in my opinion, more natural “mixing” strategy, which I hope you will find useful. I will end with a matrix of profiling results, summarizing the impact on the performance of each technique. (Many thanks to the brilliant @kitcambridge for reviewing and improving the code this post is based on!)

Reuse of functions

In JavaScript, each object refers to a prototype object from which it can inherit properties. Prototypes are excellent tools for code reuse: a single prototype instance can define properties of an infinite number of dependent entities. Prototypes can also be inherited from other prototypes, thus forming chains of prototypes that more or less follow the inheritance hierarchy of “class” languages ​​such as Java and C ++. Multi-storey inheritance hierarchies are sometimes useful in describing the natural order of things, but if the primary motive is code reuse, such hierarchies can quickly become twisted labyrinths of meaningless subclasses, tedious redundancies, and uncontrollable logic (“button is a rectangle or control? That's what, let's inherit Button from Rectangle , and Rectangle can inherit from Control ... so, stop ... ").

Fortunately, when it comes to reusing functions, JavaScript offers viable alternatives. In contrast to more rigidly structured languages, objects in JavaScript can call any public function regardless of pedigree. The most straightforward approach is delegation; Any public function can be called directly via call or apply . This is an effective feature, and I use it extensively. Anyway, delegation is so convenient that sometimes it starts working against structural code discipline; moreover, the syntax may become slightly verbose. Impurities are a great compromise that allows you to borrow and have access to whole functional units using minimalistic syntax, and they work fine in the same team with prototypes. They offer the descriptive power of hierarchical inheritance without the brain problems associated with high-rise, single-root inheritance.

The basics

In programming, an impurity is a class that defines a set of functions related to a type (for example, Person , Circle , Observer ). Impurity classes are usually considered abstract in the sense that they do not have instances on their own — instead, their methods are copied (or “borrowed”) by concrete classes as “inheritance” of behavior without entering into formal relationships with the supplier of behavior.
OK, but this is JavaScript, and we have no classes. This is, in fact, good, because it means that instead we can use objects (instances), which gives clarity and flexibility: our impurities can be an ordinary object, prototype or function — in any case, the process of “mixing” becomes transparent and obvious.
')

Using

I intend to discuss several impurity techniques, but all the code examples boil down to one use-case: creating round, oval or straight buttons. Here is a schematic view (created using the latest hi-tech gadgets). In the rectangles - impurities, in the circles - full buttons.


1. Classic impurities

Having looked at the first two pages of Google’s issue for the javascript mixin request, I noticed that most authors define impurity objects as a full-fledged type with a designer and methods defined in the prototype. You can consider this as a natural progress - previously the impurities were classes, and this is the closest to the classes that JS has. Here is a mixture of a circle created in this style:
 var Circle = function() {}; Circle.prototype = { area: function() { return Math.PI * this.radius * this.radius; }, grow: function() { this.radius++; }, shrink: function() { this.radius--; } }; 

In practice, however, such a heavy mixture is superfluous. A simple object literal is enough:
 var circleFns = { area: function() { return Math.PI * this.radius * this.radius; }, grow: function() { this.radius++; }, shrink: function() { this.radius--; } }; 


Extend function

How does such an admixture object mix into our objects? Using the extend function (sometimes called augment ). In general, extend simply copies (but does not clone) the impurity functions to the receiving object. A quick overview shows several small variations of this implementation. For example, Prototype.js skips the hasOwnProperty check (assuming the impurity does not have enumerated properties in the prototype chain), while other versions assume that you only want to copy properties from the impurity prototype. Here is a safe and flexible option ...
 function extend(destination, source) { for (var k in source) { if (source.hasOwnProperty(k)) { destination[k] = source[k]; } } return destination; } 

... which we can call to expand our prototype ...
 var RoundButton = function(radius, label) { this.radius = radius; this.label = label; }; extend(RoundButton.prototype, circleFns); extend(RoundButton.prototype, buttonFns); //etc. ... 


2. Functional impurities

If the functions defined in the impurities are intended to be used only by other objects, why create impurities at all as objects? In other words, the impurity must be a process, not an object. It would be logical to turn our impurities into functions that objects implement themselves in themselves through delegation. This eliminates the need for an intermediary - the extend function.
 var asCircle = function() { this.area = function() { return Math.PI * this.radius * this.radius; }; this.grow = function() { this.radius++; }; this.shrink = function() { this.radius--; }; return this; }; var Circle = function(radius) { this.radius = radius; }; asCircle.call(Circle.prototype); var circle1 = new Circle(5); circle1.area(); //78.54 

This approach looks right. Impurities are like verbs, not nouns; lightweight universal shops. There are other things that you might like - the style of the code is natural and concise: this always points to the recipient of the function sets, and not to an abstract object that we do not need and which we do not use; moreover, in contrast to the traditional approach, we do not need protection against unintentional copying of inherited properties, and (whatever that means) functions are now cloned, not copied.
Here is the function-impurity for the buttons:
 var asButton = function() { this.hover = function(bool) { bool ? mylib.appendClass('hover') : mylib.removeClass('hover'); }; this.press = function(bool) { bool ? mylib.appendClass('pressed') : mylib.removeClass('pressed'); }; this.fire = function() { return this.action(); }; return this; }; 

We take two impurities together and get round buttons:
 var RoundButton = function(radius, label, action) { this.radius = radius; this.label = label; this.action = action; }; asButton.call(RoundButton.prototype); asCircle.call(RoundButton.prototype); var button1 = new RoundButton(4, 'yes!', function() {return 'you said yes!'}); button1.fire(); //'you said yes!' 


3. Add options

A strategy with functions also allows parameterization of borrowed behavior — by passing an argument of options. Let's see how this works by creating an asOval impurity with custom grow and shrink parameters:
 var asOval = function(options) { this.area = function() { return Math.PI * this.longRadius * this.shortRadius; }; this.ratio = function() { return this.longRadius/this.shortRadius; }; this.grow = function() { this.shortRadius += (options.growBy/this.ratio()); this.longRadius += options.growBy; }; this.shrink = function() { this.shortRadius -= (options.shrinkBy/this.ratio()); this.longRadius -= options.shrinkBy; }; return this; } var OvalButton = function(longRadius, shortRadius, label, action) { this.longRadius = longRadius; this.shortRadius = shortRadius; this.label = label; this.action = action; }; asButton.call(OvalButton.prototype); asOval.call(OvalButton.prototype, {growBy: 2, shrinkBy: 2}); var button2 = new OvalButton(3, 2, 'send', function() {return 'message sent'}); button2.area(); //18.84955592153876 button2.grow(); button2.area(); //52.35987755982988 button2.fire(); //'message sent' 


4. Add caching

You may be concerned that this approach degrades performance, as we redefine the same functions again and again with each call. Using such a great thing as jsperf.com , I removed the metrics of all impurity strategies (the results at the end of the article). Surprisingly, in Chrome 12, the performance is higher using the functional approach, while in other browsers such impurities are twice as slow as the classic ones. Assuming that such impurities are likely to be called once for each type (and not when creating each instance), this difference should not play a significant role - given that we are talking about 26000 impurities per second, even in IE8!
Just in case, if such numbers do not allow your manager to sleep at night, here is the solution. By placing impurities in the closure, we will be able to cache the result of the determination, which will greatly affect performance. Functional impurities now easily beat the classic performance in all browsers (in my tests, about 20 times in Chrome and about 13 in FF4). Again, this is not so significant, but leaves a pleasant feeling :)
Here is the version of asRectangle with the addition of caching ...
 var asRectangle = (function() { function area() { return this.length * this.width; } function grow() { this.length++, this.width++; } function shrink() { this.length--, this.width--; } return function() { this.area = area; this.grow = grow; this.shrink = shrink; return this; }; })(); var RectangularButton = function(length, width, label, action) { this.length = length; this.width = width; this.label = label; this.action = action; } asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype); var button3 = new RectangularButton(4, 2, 'delete', function() {return 'deleted'}); button3.area(); //8 button3.grow(); button3.area(); //15 button3.fire(); //'deleted' 


5. Add Currying

In life, everything is a result of a compromise, and the aforementioned improvement with caching is no exception. We have lost the ability to create real clones of each impurity, and, moreover, we can no longer customize borrowed functions by passing parameters. The last problem can be solved by currying each cached function - thus assigning the parameters in advance, before subsequent calls. Here is an asRectangle admixture with correctly asRectangle functions that allow parameterization of grow and shrink .
 Function.prototype.curry = function() { var fn = this; var args = [].slice.call(arguments, 0); return function() { return fn.apply(this, args.concat([].slice.call(arguments, 0))); }; } var asRectangle = (function() { function area() { return this.length * this.width; } function grow(growBy) { this.length += growBy, this.width +=growBy; } function shrink(shrinkBy) { this.length -= shrinkBy, this.width -= shrinkBy; } return function(options) { this.area = area; this.grow = grow.curry(options['growBy']); this.shrink = shrink.curry(options['shrinkBy']); return this; }; })(); asButton.call(RectangularButton.prototype); asRectangle.call(RectangularButton.prototype, {growBy: 2, shrinkBy: 2}); var button4 = new RectangularButton(2, 1, 'add', function() {return 'added'}); button4.area(); //2 button4.grow(); button4.area(); //12 button4.fire(); //'added' 

Performance metrics


As promised, here are the results of the jsperf tests , tabulated by techniques and browsers.
The result is expressed in thousands of operations per second, so the higher the number, the better.

(Note of the translator: it is better to see more recent results directly on jsperf.com; the table from the original post is shown just to show the order of the numbers)

Conclusion

JavaScript is a fusion of functions and state. The state is usually specific to instances, while functions are likely to be common. It may be in our interests to separate these two basic areas of responsibility, and, possibly, admixtures will help us in this.
In particular, the pattern of functional impurities offers a clean distinction. Objects are a state, and functions are organized like clusters of fruit on a tree, ripened to be harvested. In fact, this strategy can be extended far beyond the scope of impurities - the sets of functions can serve as a repository for any object.
 var myCircle = asCircle.call({radius:25}); myCircle.area(); //1963.50 

Good luck in studying the impurities, and do not be afraid to send corrections and any other feedback!

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


All Articles