Very fast JavaScript classes with nice syntax

When writing serious projects, JavaScript programmers have a choice: sacrifice the quality of the code and write classes with their hands, or sacrifice speed and use the class system. And if you use the system, then which one to choose?

The article discusses the author’s system, which is not inferior in speed to classes written “by hand” (in other words, one of the fastest in the world). But at the same time, classes have a nice C-style structure.

Class systems


There is a joke that every programmer should write his own class system. Who is not familiar with the problem - see this comment , they collected at least 50 pieces.

Each of these bikes is distinguished by its set of features, its programming style and its drop in speed. For example, creating a MooTools class is about 90 times slower than creating a handwritten class. Why then are all these systems needed?

In practice, it turns out that handwritten classes are very difficult to maintain. When your JS application grows to a decent size, then the prototypes will cease to be as “cool” as before, and you will probably think about it: it may be worth sacrificing a little performance, but it will be easier for people to work with it. Imagine, for example, what Ext.JS written in prototypes would look like.

Note: some serious projects still do not use the class system, and it does not seem to suffer much from this. As an example - see the source Derby.js. But I perceive Derby as a black box that does something for you, so developers do not strongly encourage digging into its guts (correct, if not right); and in Ext, inheritance is very important.

System Benefits

What do we want from the system? First of all, this is a call to parent methods. Here is an example from MooTools:

var Cat = new Class({
    Extends: Animal,
    initialize: function(name, age){
        this.parent(age); // вызов родительского метода
    }
});

At first it looks very nice: inside any function you have a parent method. And it’s convenient to refactor - if you rename the method, the parent’s call will not break. But for the beauty and convenience you have to pay a big price - each method in the class will be wrapped in such a terrible package:

var wrapper = function(){
    if (method.$protected && this.$caller == null) thrownewError('The method "' + key + '" cannot be called.');
    var caller = this.caller, current = this.$caller;
    this.caller = current; this.$caller = wrapper;
    var result = method.apply(this, arguments);
    this.$caller = current; this.caller = caller;
    return result;
}.extend({$owner: self, $origin: method, $name: key});

It greatly interferes with debugging, not to mention that it is very slow - this code will be executed when any class method is called.

What else is critical? Each instance of the class must have its own properties:

var Cat = new Class({
    food: [],
    initialize: function(name){
        this.name = name;
    }
});
var cat1 = new Cat('Мурка');
var cat2 = new Cat('Мурзик');
// массивы разные
cat1.food.push('Мышь');
cat2.food.length == 0; // пустой массив

As you can see, MooTools created its own food array for each class. How would all this be done with the traditional approach? We would assign properties in the constructor:

functionCat() {
    this.food = [];
    Cat.superclass.constructor.call(this)
}
Cat.prototype.meow = function() {/*...*/}

As for the methods, there are several options, the example above shows the option with the extend function of Douglas Crockford. Under a traditional system, the code has a lot of garbage like “Cat.prototype ...” and “superclass.constructor.call (this ...)”, such code is hard to perceive and refactor.

A few words about private members of the class

What is absolutely normal in C ++ can be very harmful in JavaScript. I say this from my own experience: if classes have private methods and variables, then such classes often become unsupported. If you want to change something in such a piece of code, then sometimes you have no choice but to throw away the old code and rewrite everything from scratch.

Private members are bad practice. It is correct to have protected members (the name begins with "_"), and if you are afraid that some monkey will start to get them from the outside, then this is his business. Then it turns out that you are hiding them from the programmer who will inherit your class. Perhaps this is your goal, but most often private members do not solve anything, but only complicate the class and create problems for adequate programmers.

Now, let's create a class system that is as user-friendly as C ++, but as fast as hand-written classes. And to work without preprocessors.

Writing fast classes


So, the fastest way to create a class in JS is to write it with your hands using prototypes:

functionAnimal() {}
Animal.prototype.init = function() {}

All browser engines are optimized for this method. A step to the side - and get a drop in performance, for example:

Animal.prototype = {
    init: function() {}
}

In this example, the prototype was assigned as an object. Chrome eats this normally, but in Firefox the speed of creating classes drops significantly.

Quick inheritance

Now we need to call the parent methods. Is there anything faster than a prototype chain? And let's just rename the parent method in the derived class!

functionCat() {} // наследник Animal
Cat.prototype.Animal$init = Animal.prototype.init;
Cat.prototype.init = function() {
    this.Animal$init(); // вызов родительского метода
}

We copied the method from the parent prototype, and renamed it. Faster is no longer possible. Of course, we will not do it by hand - the class system will do everything for us.

In this example, the instanceof operator will not work, but in practice, you can do fine without it. I’m talking about real applications and tasks: if you need to distinguish the type of Animal from Cat, then this is a real task, and it is perfectly solved. But if you want to do this with the instanceof operator, then I'm sorry, you have to go to another doctor.

Even with this inheritance, there is no prototype chain (since prototypes are copied) - this gives a slight acceleration compared to traditional solutions.

Convenient Properties

Assigning default properties by hand in the constructor is also not very pleasant. So, let a script do this for us, as in MooTools. How this will work: the class system itself will generate a constructor function that will assign default properties. It will look like this:

ClassManager.define(
'Cat',
{
    Extends: 'Animal',
    food: [],
    init: function() {
        this.Animal$init();
    }
});

As a result, we get:

// сгенерированный конструкторfunctionCat() {
    this.food = [];
    this.init.apply(this, arguments);
}
// у которого будет такой прототип
Cat.prototype.Animal$init = Animal.prototype.init;
Cat.prototype.init = init: function() {
    this.Animal$init();
}

Overridden parent methods are renamed according to this rule:

<имя_класса_родителя> + "$" + <имя_метода>

Such syntax is the least we have paid for speed, and in practice it does not cause any inconvenience. And the classes themselves are nice to debase, and it's nice to look at them.

Classmanager


Now a little PR of my decision. Speed ​​test, ClassManager vs Native ( link to jsperf ):



The difference in the speed of creating classes can be attributed to the jsperf error (in old graphs it is the same for all test cases). For information: in practice, it happened to me that the same code, running as 2 different tests, was executed with a 20% difference in speed.

Why the call to the Native method is so slow - it says this:

NativeChildClass.prototype.method = function() {
    NativeParentClass.prototype.method.apply(this);
}

The difference in speed between the call from your own prototype and through apply is immediately noticeable. If it seems to you that I counted here - then write your tests, it will not be faster anyway.

Separately, it is worth mentioning about Firefox: creating a class that is generated in the browser is now much slower (on my old laptop - only 400,000 operations per second). But my ClassManager allows you to build classes on the server - and in FF they work even faster than Native. In addition, this will speed up page loading.

ClassManager vs other systems

As a basis, I took the author’s test DotNetWise, but ... his test is mean, he’s testing class generation plus 500 iterations by methods. As you understand, the quality and speed of the generated code does not depend on the time of its generation, and for each tested framework this time introduces its own error. Moreover, my classes can be assembled on the server.

So it will be much more fair to first create classes, and then test them. And if you need to compare the generation time of classes, then it will be correct to create a separate test for this, and not to add it to the speed of method calls.

In the original test - the author’s system DNW, of course, leads. But if you fix the test, then in Chrome, my ClassManager will come first, followed by Fiber, and then DNW. In FF, TypeScript comes first, then Native, then ClassManager. Even so, this is a very specific test - here the creation of a class is measured along with the call of methods (in the wrong proportions), so I believe that it does not reflect the real picture. However, here is the link and the results:



ClassManager Features


I'll start with a very important detail: IDE tooltips work for my classes! At least in most cases (I use PhpStorm). Here is an example of how classes might look:

Lava.ClassManager.define(
// все классы лежат в пространствах имен, даже глобальные'Lava.Animal',
{
	// добавляет методы on(), _fire() и другие
	Extends: 'Lava.mixin.Observable',
	// можно и так:// Implements: 'Lava.mixin.Observable',
	name: null,
	toys: [], // для каждого экземпляра - свой массив
	init: function(name) {
		this.name = name;
	},
	takeToy: function(toy) {
		this.toys.push(toy)
	}
});
Lava.ClassManager.define(
'Lava.Cat',
{
	Extends: 'Lava.Animal',
	// перечисляем имена объектов, которые будут вынесены в прототип
	Shared: ['_shared'],
	// этот объект будет вынесен в прототип, он станет общим для всех классов
	_shared: {
		likes_food: ['мышь', 'вискас']
	},
	breed: null,
	init: function(name, breed) {
		this.Animal$init(name);
		this.breed = breed;
	},
	eat: function(food) {
		if (this._shared.likes_food.indexOf(food) != -1) {
			// отправляем событие, метод из Lava.mixin.Observablethis._fire('eaten', food);
		}
	}
});
var cat = new Lava.Cat('Гарфилд', 'Персидская');
// добавляем слушатель - такую возможность предоставил нам Lava.mixin.Observable
cat.on('eaten', function(garfield, food) {
	console.log('Гарфилд сьел ' + food);
}, {});
cat.eat('мышь'); // выведет в консоль "Гарфилд сьел мышь"


Standard Directives:
  1. Extends - direct inheritance. A descendant can be inherited from only one parent.
  2. Implements - for mixins and multiple inheritance. It holds in the descendant properties and methods from the mixin, but everything that is redefined in the class takes precedence.
  3. Shared - brings an object to a prototype. By default, all objects in the body of the class are copied for each instance, but they can be shared.

Bonuses:
  1. It is possible to patch class methods on the fly and static constructors. For example, you want to apply a bugfix inside IE, and disable it in other browsers. In the class constructor, you can select the method you need and replace it in the prototype - even if your class is in the middle of the inheritance chain.
  2. Export generated classes. You can generate constructors on the server - this will save page load time and speed up the creation of objects in Firefox.
  3. Namespaces and packages. Read the documentation for details.

The plans are to add such modifiers as abstract and final.

Disadvantages:
  1. Now the Shared directive is able to transfer only objects (not arrays) to the prototype. As a temporary solution, you can create an object with an array property, so this is just a slight inconvenience. There is a task for revision, but it is not yet a priority.
  2. And a more noticeable drawback: now there is no tool that could compress the names of class members (if you simply rename them, then the call to the parent methods will break). There are plans to create it, it will certainly appear, but not tomorrow. Interestingly, if I did not say this myself, would you pay attention to it?

Where to get?

The standalone version is in this repository . It also has a link to the main framework website - there you will find excellent documentation (in English), you can also see examples in the code, and take several universal classes such as Observable (events), Properties (properties with events) and Enumerable (“live "Array).

PS
Yes, by the way: the main framework is called LiquidLava, and it was created as the best alternative to Angular and Ember. Interesting?

UPD
In the comments, they corrected me: the speed of calling the Native method can be increased if you replace apply with call. The first ClassManager vs Native test was updated: in FF, the speed of calling the Native method was equal to the speed of ClassManager, and in Chrome it is still slightly inferior.

Also popular now: