es7 decorators in depth or not

Mixins are dead (or should be) and we could say that ES7 decorators (experimental) will provide a new way of changing object behaviours at runtime, as they give a new opportunity to modify classes or properties. Current specification is a good start to play with decorators, but I 'll try to share a little bit more with you about it.

Using it

ES7 decorators syntax is trivial.

@decoratorName(optionalParams)
class MyClass {

  @decoratorName(optionalParams)
  targetObjectMethodOrProperty

}

Note that decorators do not work on a single function outside a class definition:

@celerity(40)
function move() {
}

// Error => Leading decorators must be attached to a class declaration

Design

We will cover some examples later in this post but implementing a decorator is done by creating a simple function:

// example on a method with a decorator named 'dec'
@dec
method() {}

// decorator definition
function dec(target, name, descriptor) {}
// or 
function dec(target, name) {}
// or
function dec(target) {}

As you can see, a decorator can take up to 3 arguments [and optionally returns a decorator descriptor to install on the target object]

Parameters

target : depending of your use it can be

  • current object Constructor
  • current object Prototype
  • directly current object when decorator is on an object literals

name: current property name or null when decorator is on Class

descriptor : current property descriptor or null when decorator is on Class

Decorator as a factory You may need to pass parameters (function, object, primitive type) to your decorator and in this case it will become a factory:

@dec
method() {}

// and your decorator function
function dec(tax /*...moreparams*/) {
  return function(target, name, descriptor) {
     target.price = target.price * tax;
    }
}

But how decorators works ?

I will try to explain it :)

A decorator function is quite similar to native Object.defineProperty() method.

A decorator can be "attached" to :

  • Class Constructor function
  • Class Properties : object, method, accessors

A decorator will precede the syntax that defines a property (or method).

@classDecorator
class Clazz {

  @staticPropDecorator
  static prop = 5;

  @staticMethodDecorator    
  static fnStatic = function() {}

  @propDecorator
  prop = 5;

  @methodDecorator
  fn() {}

}

Life before ES7 decorator

Here is an example of how we could implement job done by decorators before they occur in ES7.

Let's create a class that contains a single method.

class C { 
  method(...params) {
    console.log(params);
  }
}
const c = new C();
c.method('a', 'b', 'c'); 
// ['a','b','c']

Exercice consist in applying (call a function) something before and after the call to 'method()' function.

To do that, we will use defineProperty and getOwnPropertyDescriptor functions.

First step consist it retrieving method property descriptor object and then transform it.

// get descriptor of method() property
const methodDesc = Object.getOwnPropertyDescriptor(C.prototype, 'method');
// {
//   'writable':true,
//   'enumerable':true,
//   'configurable':true,
// value field is our function
//   'value': function method() {
//     console.log(params);
//   }

Now that we have property descriptor in hand, why not patch its value property ?

To do this , we create a function similar to that of a definition of decorators in ES7.

function patchMethod(target, key, descriptor) {
  // store old method
  const oldMethod = descriptor.value;

  // replace by new one
  descriptor.value = function() {
    console.log('before');
    oldMethod.apply(this, arguments);
    console.log('after');
  }

  // reapply on property
  Object.defineProperty(target, key, descriptor);
}

Our main method will look like

const methodDesc = Object.getOwnPropertyDescriptor(C.prototype, 'method');
// patch decorator
patchFunction(C.prototype, 'method', methodDesc );

const c = new C();
c.method('a', 'b', 'c');
// before
// ["a","b","c"]
// after

That was basically how decorator will work.

Class and static properties

Before digging with Class Constructor decorators, let's recap how static keyword works in ES7 class (experimental, see BabelJS).

One way of adding static property or method in javascript ES6 is given by affecting the class itself meaning on constructor property rather than on instance's prototype.

Class with static properties example:

class Circle {
  // attach to Circle prototype constructor function 
  static name = 'circle';

  // attach to Circle prototype constructor function
  static circumference = function(r) {
   return 2 * Math.PI * r;
  }

  constructor() {
  // <= constructor function that will receive static props/methods
  }
}

const circle = new Circle();
circle.name;        // won't work
circle.circumference(2.5);  // won't work

Circle.name;         // circle
Circle.circumference(2.5); // 15.707963267948966

Static properties or methods are bound to the class constructor function and not to the instance's prototype object.

And as you could expect, circumference() method won't get access to instance fields, properties or methods of an instance of this class Circle.

Same version in ES5 would be

function Circle() {}

Circle.name = 'circle';

Circle.circumference = function(radius) {
 return 2 * Math.PI * radius;
};

First way will affect the class itself meaining the constructor rather than the prototype .

Decorator on Class Constructor

As seen in previous paragraph with static properties, we could express the same result with a global decorator on class that will enhance class constructor with some properties or methods.

@circleUtilities
class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  getCircumference() {
    return Circle.circumference(this.radius);
  }
}

and decorator would be

function circleUtilities(target, key, descriptor) {
  Object.assign(target, {
    displayName: 'circle',
    circumference: function(r) {
      return 2 * Math.PI * r;
    },
    area: function(r) {
      return Math.PI * Math.pow(r,2);
    }
  });
}

// Constructor method/prop
console.log(Circle.displayName);          
// 'circle'
console.log(Circle.circumference(2.5)); 
// 15.707963267948966

// Prototype method/prop
const c = new Circle(2.5);
console.log(c.radius); 
// 2.5
console.log(c.getCircumference()); 
// 15.707963267948966
console.log(c.area());
// won't work

Attached to Constructor function (static way) in this case and not instance Prototype.

Decorator on Class Prototype

If you want to add properties on Prototype instead of Constructor (static), you only need to modify decorator to work with prototype instead of constructor function.

function circleUtilities(target, key, descriptor) {
  Object.assign(target.prototype, {
   circumference: function() {
    return 2 * Math.PI * this.radius;
     },
     area() {
    return Math.PI * Math.pow(this.radius,2);
     }
  });
}

const c = new Circle(2.5);
console.log(c.circumference());

Decorator on accessor property

You may want to attach a decorator to an accessor and free to you to do what you want then

class C {

    @prefix('my name is: ')
    get name() {
      return this._name;
    }

    set name(newName) {
      this._name = newName;
    }
}

function prefix(string) {
  return function(target, key, descriptor) {
    let getter = descriptor.get;

    descriptor.get = function() {
      return string + getter.call(this);
    }
  }  
}

const c = new C();
c.name = 'julien';

console.log(c.name);
// my name is: julien

Another decorator on method property

Here another sample with a decorator that take a function as parameter.

const prefix = s => 'my fullname is ' + s;
const suffix = s => s + ' is my fullname';

class Person {

  constructor(first, last) {
    this.first = first;
    this.last = last;
  }

  name() { 
    return `${this.first} ${this.last}`;
  }

  @transformString(prefix)
  namePrefixed() { 
    return this.name();
  }

  @transformString(suffix)
  nameSuffixed() { 
    return this.name();
  }
}

function transformString(fn) {
  return function(target, key, descriptor) {
    const oldValue = descriptor.value;

    descriptor.value = function() {
      const s = oldValue.call(this);
      return fn(s);
    }

  }  
}

const t = new Person('julien', 'valery');

console.log(t.namePrefixed());
console.log(t.nameSuffixed());
// my fullname is julien valery
// julien valery is my fullname

Dependency injection example

Decorator is really a good option to create declarative code that solve dependency injection a well known pattern.

In a nutshell, you let a container to manage component instances and just declare component dependencies to be injected in constructor (or method) at runtime.

Usage

Goal of this example is to allow that kind of syntax with a @inject decorator

@inject([DependencyOne, DependencyTwo...])
class Component {

  constructor(serviceOne, serviceTwo) {
  this.serviceOne = serviceOne;
    this.serviceTwo= serviceTwo;
  }

  method() {
    this.serviceOne....
  }

We could then add a @singleton decorator that will be handle by container to resolve your dependency only one time.

@singleton
class DependencyOne{
  fetchSomeData(cb) {
    cb({data: 'ServiceTwo'});
  }
}

Full code

Here is what Container code may look like

class Container {

  static singletons = new Map();

  static registerSingleton(clazz) {
    if (Container.singletons.has(clazz)) {
      return Container.singletons.get(clazz);
    }

    const instance = Container.resolve(clazz);

    Container.singletons.set(clazz, instance);

    return instance;
  }

  static resolve(clazz) {
    const store = Container.store;

    const depKeys = clazz.dependencies;

    let dependencies = [];

    if (depKeys) {    
       dependencies = depKeys.reduce(function(deps, depClazz) {

         if (depClazz.singleton) {
           deps.push(Container.registerSingleton(depClazz));
         }
         else {
           deps.push(new depClazz());
         }

        return deps;

      }, []);
    }

    return new clazz(...dependencies);
  }

}

Then your main code

@singleton
class ServiceOne {
  fetchSomeData(cb) {
    cb({data: 'ServiceOne'});
  }
}

@singleton
class ServiceTwo {
  fetchSomeData(cb) {
    cb({data: 'ServiceTwo'});
  }
}

@inject([ServiceOne])
class Component {
  constructor(service) {
    this.service = service;
  }

  render() {
    console.log('rendering component one');
    this.service.fetchSomeData(function(data) {
      console.log(data);
    });
  }
}

@inject([ServiceOne, ServiceTwo])
class ComponentTwo {
  constructor(s1, s2) {
    this.serviceOne = s1;
    this.serviceTwo = s2;
  }

  render() {
    console.log('rendering component two');
    this.serviceOne.fetchSomeData(function(data) {
      console.log(data);
    });
    this.serviceTwo.fetchSomeData(function(data) {
      console.log(data);
    });
  }
}

// @singleton decorator
function singleton(target, key, descriptor) {
  target.singleton = true;
}

// @inject decorator
function inject(dependencies) {

  return function(target, key, descriptor) {
    target.dependencies = dependencies;
  }
}

const c1 = Container.resolve(Component);
const c2 = Container.resolve(ComponentTwo);
c1.render();
c2.render();
// rendering component one
// {"data":"ServiceOne"}
// rendering component two
// {"data":"ServiceOne"}
// {"data":"ServiceTwo"}

Conclusion

This post is long but I hope it will give you a new overview and ideas of what can be done with awesome ES7 decorators.

Below a lot of links you may read too.

References

es6-classes-final

static in js

Wycats specification more

Dependency injection framework with ES7 decorators

Class extension

Angular with decorators French

Popular React autobind decorator

Common js useful decorators

Short example


Tags: ES6 ES7 Javascript Angular