Function wrapping with Javascript Decorators

One of the best uses for ES7/Javascript 2017 decorators is adding additional functionality to a function. This article will discuss the basics of decorators and show some patterns that can be used to help write cleaner code.

Note: ES7/Javascript 2017 decorators are still in proposal, so this may be subject to change. At the time of this writing, Oct 2015, Decorators work with Babel version 5.8.23.

For starters, decorators must be attached to objects. They work by mutating the property definition in a similar fashion to directly defining a property via defineProperty.

To facilitate this, decorator functions accept the following arguments:
1) target - the object that owns the property
2) name - the name of the property
3) descriptor - the property descriptor used in defineProperty

Of particular note in the descriptor is the value property. This property contains the function definition.

With the ability to override the descriptor and with access to the default target object, you can override the value property and extend functionality by wrapping the original function in additional logic.

Basic function wrapping

Here is an example of using a decorator to add start and end logging to a function. Our basic function has been decorated with the logger function.

let example = {  
  @logger
  logMe() {
    console.log('I want to be logged');
  }
};

We need to define a decorator function that will add start and end logging. This will override the value property on the descriptor.

// Decorator function for logging
function logger(target, name, descriptor) {

  // obtain the original function
  let fn = descriptor.value;

  // create a new function that sandwiches
  // the call to our original function between
  // two logging statements
  let newFn  = function() {
    console.log('starting %s', name);
    fn.apply(target, arguments);
    console.log('ending %s', name);
  };

  // we then overwrite the origin descriptor value
  // and return the new descriptor
  descriptor.value = newFn;
  return descriptor;
}

When called this will produce the following output.

$ babel-node --stage 1 logger.js
starting logMe  
I want to be logged  
ending logMe  

So here is what is going on with the decorator function. The original function definition is contained in descriptor.value. This is what gets invoked.

We can overwrite this property with a new function that applies our own logic and still invokes the original function! In order to call the original function will use apply to bind the original function to our target and pass arguments.

Customization

That's all well and good, but what if we want to customize the message? In order to do this, we simply turn our decorator function into a factory function and use closure scope to pass in our customizations via arguments. What?!? Here, take a look at this...

What is we want to customize the message by passing it to the decorator?

let example = {  
  @logger('custom message starting %s', 'custom message ending %s')
  logMe() {
    console.log('I want to be logged');
  }
};

To accomodate this, our decorator function accepts two arguments and immediately returns the decorator function.

// Decorator function for logging that accepts custom arguments
function logger(startMsg, endMsg) {  
  return function(target, name, descriptor) {

    // obtain the original function
    let fn = descriptor.value;

    // create a new function that sandwiches
    // the call to our original function between
    // two logging statements
    let newFn  = function() {
      console.log(startMsg, name);
      fn.apply(target, arguments);
      console.log(endMsg, name);
    };

    // we then overwrite the origin descriptor value
    // and return the new descriptor
    descriptor.value = newFn;
    return descriptor;
  }
}

When called, this produces the output:

$ babel-node --stage 1 better-logger.js
custom message starting logMe  
I want to be logged  
custom message ending logMe  

You can continue to get fancier or create a wrapping utility to reduce the boilerplate... but that's the jist of it! Happy coding.

For a full working example, checkout the project on Github https://github.com/bmancini55/javascript-decorators

comments powered by Disqus