We disassemble decorators ES2016



Many of us are probably already tired of this hype around the latest ECMAScript standards. ES6, ES7 ECMAScript Harmony ... It seems that everyone has their own opinion on how to properly call JS. But even with all this hype, what is happening with JavaScript right now is the most remarkable thing that has happened to it over the past 5 years, at least. The language lives, develops, the community constantly offers new features and syntactic constructions. One of these new designs that certainly deserve attention is decorators. Having searched for materials on this topic, I realized that there is practically nothing about decorators on the Russian-speaking Internet. At the same time, Addy Osmani in July 2015 presented an excellent article Exploring ES2016 Decoratorson Medium. In this regard, I would like to bring to your attention a translation of this article into Russian and post it here.

Iterators , generators , list inclusions ... With each innovation, the differences between JavaScript and Python are becoming smaller. Today we’ll talk about another “python-like” proposal for the ECMAScript 2016 standard (aka ES7) - Decorators by Yehuda Katz.

Decorator Pattern


Before analyzing the scope and rules for using decorators for ES2016, let's still find out if there is something similar in other languages? For example in Python. For him, decorators are just syntactic sugar that makes it easy to call higher-order functions . In other words, it’s just a function that takes another function, changes its behavior, while not making changes to its source code . The simplest decorator we could imagine as shown below:

@mydecorator
def myfunc():
  pass

The expression at the very top of the example ("@mydecorator") is a decorator whose syntax will look exactly the same in the ES2016 (ES7) standard, so you have already mastered some of the necessary material.

The @ symbol tells the parser that we are using a decorator, while mydecorator is just the name of some function. Our decorator from the example takes one parameter (namely, the decorated function myfunc ) and returns exactly the same function, but with the changed functionality.

Decorators are very useful if you want to add extra behavior to a function without changing its source code. For example, in order to provide support for memoization, access levels, authentication, instrumentation, logging ... there are really many use cases, this list goes on and on.

Decorators in ES5 and ES2015 (aka ES6)


Implementing a decorator in ES5 using an imperative approach (that is, using pure functions) is a fairly trivial task. But because ES2015 introduced native class support, to provide the same goals, we need something more flexible than just pure functions.

The proposal by Yehuda Katz to add decorators to the next ECMAScript standard provides annotation and modification of object literals, classes and their properties during code design, while using a declarative rather than imperative approach.

Let's look at some ES2016 decorators with a real example!

ES2016 decorators in action


So, let's remember what we learned from Python and try to transfer our knowledge to the field of JavaScript. As I said, the syntax for ES7 will not be any different, the main idea, too. Thus, a decorator in ES2016 is some expression that can take a target, a name, and a descriptor as arguments . Then you simply “apply” it by adding the “@” symbol at the beginning and place it right in front of the section of code that you are going to decorate. Today, decorators can be defined either to define a class or to define a property.

Decorate the property


Let's look at the class:

class Cat {
  meow() { return `${this.name} says Meow!`}
}

When we add this method to the prototype of the Cat class, we get something similar to this:

  Object.defineProperty(Cat.prototype, 'meow', {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
  });

Thus, a method is hung on the prototype of the class, allowing the cat to cast a voice. At the same time, this method is not involved in enumerating the properties of an object, it can also be changed, and it is writable. But imagine that you would like to explicitly prohibit changes to the implementation of this method. For example, we could do this using the "@readonly" decorator, which, as the name implies, would give access to this read-only method and prevent any overrides:

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

And then we add the created decorator to the method, as shown below:

  class Cat {
    @readonly
    meow() { return `${this.name} says Meow!`}
  }

The decorator can also accept parameters, in the end the decorator is just an expression, so both "@readonly" and "@something (parameter)" should work.

Now before hanging the method on a prototype of the Cat class, the engine will launch the decorator:

  let descriptor = {
    value: specifiedFunction,
    enumerable: false,
    configurable: true,
    writable: true
  };
  // Декоратор имеет ту же сигнатуру, что и "Object.defineProperty",
  // Поэтому мы без труда можем переопределить дескриптор, пока 
  // "Object.defineProperty" не вызван
  descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
  Object.defineProperty(Cat.prototype, 'meow', descriptor); 

Thus, the decorated meow method becomes read-only. We can check this behavior:

  let garfield = new Cat();
  garfield.meow = () => { console.log('I want lasagne!'); };
  // Ошибка! Так как мы не можем изменять реализацию метода.

Interesting, isn't it? Soon we will look at decorating classes, but before that, let's talk about community support for decorators. Despite their immaturity, whole libraries of decorators begin to appear (for example, https://github.com/jayphelps/core-decorators.js from Jay Phelps). Just as we wrote our decorator, you can take exactly the same, but implemented in the library:

  import { readonly } from 'core-decorators';
  class Meal {
    @readonly
    entree = 'steak';
  }
  let meal = new Meal();
  meal.entree = 'salmon';
  // Ошибка!

The library is also good because it implements the "@deprecate" decorator, which is quite useful when you change the API, but you need to keep the obsolete methods for backward compatibility. Here's what the documentation says about this decorator:
The decorator calls console.warn (), and displays a warning message. This message can be redefined. Also supports adding links for future reference.


  import { deprecate } from 'core-decorators';
  class Person {
    @deprecate
    facepalm() {}
    @deprecate('We are stopped facepalming.')
    facepalmHard() {}
    @deprecate('We are stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
    facepalmHarder() {}
  }
  let captainPicard = new Person();
  captainPicard.facepalm();
  // DEPRECATION Person#facepalm will be removed in future versions
  captainPicard.facepalmHard();
  // DEPRECATION Person#facepalmHard: We are stopped facepalming.
  captainPicard.facepalmHarder();
  // DEPRECATION Person#facepalmHarder: We are stopped facepalming.
  //
  // See http://knowyourmemes.com/memes/facepalm for more details
  //

Decorate the class


Next, let's look at decorating classes. According to the proposed specification, the decorator accepts the target constructor. For example, we could create a superhero decorator for the MySuperHero class:

function superhero(target) {
  target.isSuperhero = true;
  target.power = 'flight';
}
@superhero
class MySuperHero {}
console.log(MySuperHero.isSuperhero);      // true

We can go even further and enable our decorator to accept parameters, turning it into a factory method:

function superhero(isSuperhero) {
  return function (target) {
    target.isSuperhero = isSuperHero;
    target.power = 'flight';
  }
}
@superhero(false)
class MySuperHero {}
console.log(MySuperHero.isSuperhero);      // false

So, decorators in ES2016 can work with classes and property descriptors. And, as we already found out, the property descriptor and the target object are automatically transferred to them. Having access to the property descriptor, a decorator can do such things as adding a getter to a property, adding behavior that might look cumbersome without putting it into the decorator, for example, automatically changing the context of a function call to the current entity on the first attempt to access the property.

ES2016 decorators and mixers


I carefully read the article by Reg Braithwaite ES2016 Decorators as mixins and its previous Functional Mixins and came up with a pretty interesting option for using decorators. His suggestion is that we introduce some kind of helper, which mixes the behavior with a prototype or an object of a certain class. At the same time, the functional mixin that mixes the behavior of the object with the prototype of the class looks like this:

  function mixin(behaviour, sharedBehaviour = {}) {
    const instanceKeys = Reflect.ownKeys(behaviour);
    const sharedKeys = Reflect.ownKeys(sharedBehaviour);
    const typeTag = Symbol(‘isa’);
   function _mixin(clazz) {
      for (let property of instanceKeys) {
        Object.defineProperty(clazz.prototype, property, { value: behaviour[property] });
      }
      Object.defineProperty(clazz.prototype, typeTag, { value: true});
      return clazz;
    }
    for(let property of sharedKeys) {
      Object.defineProperty(_mixin, property, {
        value: sharedBehaviour[property],
        enumerable: sharedBehaviour.propertyIsEnumerable(property)
      });
    }
    Object.defineProperty(_mixin, Symbol.hasInstance, {
      value: (i) => !!i[typeTag]
    });
    return _mixin;
  }

Excellent. Now we can define some mixins and try to decorate them with a class. Let's imagine that we have a “ComicBookCharacter” class:

  class ComicBookCharacter {
    constructor(first, last) {
      this.firstName = first;
      this.lastName = last;
    }
    realName() {
      return this.firstName + ‘ ‘ + this.lastName;
    }
  }

It may be the most boring hero of the comics, but we can save the situation by announcing several mixins that will add “superpower” to our hero and “multipurpose betman’s belt” to our hero. To do this, let's use the mixin factory announced above:

  const SuperPowers = mixin({
    addPower(name) {
      this.powers().push(name);
      return this;
    },
    powers() {
      return this._powers_pocessed || (this._powers_pocessed = []);
    }
  });
  const UtilityBelt = mixin({
    addToBelt(name) {
      this.utilities().push(name);
      return this;
    },
    utilties() {
      return this._utility_items || (this._utility_items = []);
    }
  });

Now we can use the @ syntax with the names of our mixins in order to decorate the “ComicBookCharacter” class declared above, adding the desired behavior to it. Please note that we can define several decoration instructions together:

  @SuperPowers
  @UtilityBelt
  class ComicBookCharacter {
    constructor(first, last) {
      this.firstName = first;
      this.lastName = last;
    }
    realName() {
      return this.firstName + ‘ ‘ + this.lastName;
    }
  }

Now we can create our own Batman:

  const batman = new ComicBookCharacter(‘Bruce’, ‘Wayne’);
  console.log(batman.realName());
  // Bruce Wayne
  batman
      .addToBelt(‘batarang’)
      .addToBelt(‘cape’);
  console.log(batman.utilities());
  // [‘batarang’, ‘cape’]
  batman
       .addPower(‘detective’)
       .addPower(‘voice sounds like Gollum has asthma’);
  console.log(batman.powers());
  // [‘detective’, ‘voice sounds like Gollum has asthma’]

As you can see, decorators of this kind are relatively compact and I can use them as an alternative to calling functions or as helper'ov for higher-order components.

Making Babel understand decorators


Decorators (at the time of writing) are still not approved and only proposed to be added to the standard. But due to the fact that Babel supports the transpilation of experimental designs, we can make friends with decorators.

If you use the Babel CLI, you can enable decorators like this:
  babel --optional es7.decorators

Or you can enable decorator support using a transformer:

  babel.transform(‘code’, { optional: [‘es7.decorators’] });

Well, in the end, you can play with them in the Babel REPL (for this, enable support for experimental designs).

Why aren't you using decorators in your projects yet?


In the short term, decorators in ES2016 are pretty useful for declaratively decorating, annotating, type checking, and mixing behavior with classes from ES2015. In the long run, they can serve as a very useful tool for static analysis (which can serve as an impetus for the creation of type checking tools at compile time or auto-completion).

They are no different from decorators from the classical OOP, where an object can be decorated with a specific behavior using a pattern, either statically or dynamically without affecting other objects of the same class. Nevertheless, I consider them a pleasant addition. The semantics of decorators for classes still look a little dank, but you should look at the Yehuda repository from time to time and stay tuned.

useful links


https://github.com/wycats/javascript-decorators
https://github.com/jayphelps/core-decorators.js-------->https://github.com/jayphelps/core-decorators.js
http: // blog .developsuperpowers.com / eli5-ecmascript-7-decorators /
http://elmasse.github.io/js/decorators-bindings-es7.html
http://raganwald.com/2015/06/26/decorators-in- es7.html ”> http://raganwald.com/2015/06/26/decorators-in-es7.html

Also popular now: