📜 ⬆️ ⬇️

JavaScript Decorators

Given the introduction of the ES2015 + standard, and the fact that nowadays transfiguration is common, many programmers are faced with new JavaScript features in real code and in training materials. One of these possibilities is decorators. In all these innovations it is not surprising and confused, so today we will talk about what decorators are and how to use them to make the code cleaner and clearer.

image

Decorators have gained popularity due to their use in Angular 2+. In Angular, this functionality is implemented by means of TypeScript. Now the proposal for the introduction of decorators in JavaScript is in a state of Stage 2 Draft . This means that work on them has been largely completed, but they are still subject to change. Decorators should be part of the next language update.

What is a decorator?


In its simplest form, a decorator is a way of wrapping one code fragment into another. Literally - “decorating” a snippet of code.
')
You may have heard about this concept earlier, as “Functional Composition” or “Higher Order Functions”.

This is quite realizable by standard JavaScript tools. It looks like a call to some function that wraps another:

function doSomething(name) {  console.log('Hello, ' + name); } 

 function loggingDecorator(wrapped) { return function() {   console.log('Starting');   const result = wrapped.apply(this, arguments);   console.log('Finished');   return result; } } 

 const wrapped = loggingDecorator(doSomething); 

This example shows how to create a new function that is assigned to the wrapped constant. This function can be called in the same way as the function doSomething , and it will do the same. The difference is that before and after calling the wrapped function will be logged. This is what happens if you experiment with the doSomething and wrapped functions.

 doSomething('Graham'); // Hello, Graham wrapped('Graham'); // Starting // Hello, Graham // Finished 

How to use decorators in javascript?


JavaScript decorators use a special syntax, they have a prefix in the form of an @ symbol, they are placed immediately before the code they want to decorate.

You can apply as many decorators as you need to one fragment of code, they will be used in the order in which they appear in the code.

For example:

 @log() @immutable() class Example { @time('demo') doSomething() { } } 

Here is the class declaration and the use of three decorators. Two of them belong to the class itself, and one to the class property. Here are the roles of these decorators:


Today, the use of decorators requires the use of a transpiler, since neither browsers nor Node.js yet support them.

If you use Babel, you can refer to the transform-decorators-legacy plugin for working with decorators.

Please note that the name of this plugin uses the word "legacy", which can be interpreted as an indication of some outdated technology. The point here is that it supports the way the decorators process Babel 5. This approach may differ from the form which, as a result, will be standardized.

Why do we need decorators?


Functional composition in JavaScript is implemented without any problems by standard means. However, the same approach is either very difficult or impossible to apply to other software constructs, for example, to classes and their properties. The proposed innovations allow the use of decorators and with classes, and with their properties. Probably, in future versions of JavaScript, we can expect further development of decorators.

The use of decorators, among other things, means a clearer syntax, this leads to a clearer expression of the intentions of the programmer using this technique.

Different types of decorators


Currently supported decorator types are decorators of classes and class members — properties, methods, getters, and setters. In fact, decorators are just functions that are called with some information about the elements to be decorated and return other functions.

Decorators are executed once when the program is started, the code being decorated is replaced with the return value.

â–Ť Class Members Decorators


Class member decorators are applied to properties, methods, getters, and setters.

These decorator functions are called with three parameters:


Here is a classic example that demonstrates the use of the @readonly decorator. This decorator is implemented as follows:

 function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor; } 

The decorator sets the writable flag of the writable descriptor to false .

This decorator is then used with class members as follows:

 class Example { a() {} @readonly b() {} } 

 const e = new Example(); ea = 1; eb = 2; // TypeError: Cannot assign to read only property 'b' of object '#<Example>' 

As seen in the error message, the decorator worked as expected. We can go further, for example, by changing the behavior of the function being decorated, in fact, replacing it with a new function. Let's, using the decorator, log the function arguments and what it returns:

 function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') {   descriptor.value = function(...args) {     console.log(`Arguments: ${args}`);     try {       const result = original.apply(this, args);       console.log(`Result: ${result}`);       return result;     } catch (e) {       console.log(`Error: ${e}`);       throw e;     }   } } return descriptor; } 

This construct replaces the method with a new one, which logs the arguments, calls the original method, and then logs what it returns.

Note that here we used the extension operator to automatically create an array with all the arguments passed to the method. This is a modern alternative to the property of the arguments function.

Let's look at all this in action:

 class Example {   @log   sum(a, b) {       return a + b;   } } 

 const e = new Example(); e.sum(1, 2); // Arguments: 1,2 // Result: 3 

You may notice that the text of the function decorator has to use an unusual syntax for the execution of the decorated method. About this, in fact, you can write a whole article, but, briefly, the apply method allows you to call a function, setting the value to this and the arguments that will be passed to it.

Going further, you can arrange everything so that the decorator takes the arguments for their own purposes. For example, rewrite the log decorator as follows:

 function log(name) { return function decorator(t, n, descriptor) {   const original = descriptor.value;   if (typeof original === 'function') {     descriptor.value = function(...args) {       console.log(`Arguments for ${name}: ${args}`);       try {         const result = original.apply(this, args);         console.log(`Result from ${name}: ${result}`);         return result;       } catch (e) {         console.log(`Error from ${name}: ${e}`);         throw e;       }     }   }   return descriptor; }; } 

The code is complicated, but if you look at it, it turns out that the following happens here:


The returned function is identical to the log decorator, which we described above, except that it uses the name parameter from the external function.

You can use all this like this:

 class Example { @log('some tag') sum(a, b) {   return a + b; } } 

 const e = new Example(); e.sum(1, 2); // Arguments for some tag: 1,2 // Result from some tag: 3 

It is immediately evident that this approach allows us to distinguish log lines due to the assigned tag.

Here you call a function of the form log('some tag') , and then what was returned from this call is used as a decorator for the sum method.

Class Decorators


Class decorators apply to the entire class definition. The decorator function is called with a single parameter, which is the class's constructor function being decorated.

Notice that the decorator is applied to the constructor function, and not to each instance of the class at the time of its creation. Therefore, in order to decorate different instances of the same class in different ways, it will be necessary, before creating them, to take care of decorating the constructor yourself.

In general, class decorators are less useful than class member decorators, since all that can be done here comes down to replacing the class constructor.

Returning to the logging example, we will write a decorator, which will display the parameters of the constructor:

 function log(Class) { return (...args) => {   console.log(args);   return new Class(...args); }; } 

Here, as an argument, the class is accepted and a new function is returned, which will act as a constructor. In our case, it simply logs the arguments and returns a new instance of the class created with these arguments.

For example:

 @log class Example { constructor(name, age) { } } 

 const e = new Example('Graham', 34); // [ 'Graham', 34 ] console.log(e); // Example {} 

As you can see, when the Example class constructor is executed, the constructor arguments will be logged, which are used when creating an instance of this class. This is exactly what we wanted.

To pass the parameters to the class decorators, you can use the approach already described:

 function log(name) { return function decorator(Class) {   return (...args) => {     console.log(`Arguments for ${name}: args`);     return new Class(...args);   }; } } 

 @log('Demo') class Example { constructor(name, age) {} } 

 const e = new Example('Graham', 34); // Arguments for Demo: args console.log(e); // Example {} 

Decorators in real projects


Here are some examples of using decorators in popular libraries.

Core Core Decorators Library


There is an excellent library of Core Decorators , which provides ready-to-use decorators for general use. Among the functionality they support is the timing of method calls, notification of obsolete constructs, and checking whether an object is immutable.

Library React


React has found a good use of the concept of higher order components. These are React components, written as functions and serving as wrappers for other components.

They are ideal candidates for use as decorators, since it will take minimal effort to use them as such. For example, the Redux library has a connect function, which is used to connect React components. Without decorators, working with this function may look like this:

 class MyReactComponent extends React.Component {} export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent); 

If you rewrite it using decorators, you get the following:

 @connect(mapStateToProps, mapDispatchToProps) export default class MyReactComponent extends React.Component {} 

The functionality turned out to be the same, but it all looks much nicer.

Mo MobX Library


Decorators are widely used in the MobX library. For example, to ensure the desired behavior of the system, you simply add Observable or Computed decorators to the fields, and use the Observers decorator with classes.

Results


We talked about how to create and use decorators in JavaScript, in particular - examined the features of working with decorators of class members. This approach allows you to write auxiliary code, represented by decorator functions, which can be used to change the behavior of methods of different classes. The syntax of the decorators allows you to simplify the texts of programs, make them cleaner and clearer. It is possible to work with decorators in JavaScript today, they have found application in popular libraries. However, we believe that after browsers and Node.js directly support them, they will have many new fans.

Dear readers! Do you already use decorators in your JS projects?

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


All Articles