ECMAScript decorators on functions

Posted on:

This one particular question is being often raised around the usage of the new ECMAScript decorators proposal:

why can't I use it for regular function declarations?

Well, the answer is simple: hoisting :(

Since JS hoists up function declarations, those expressions that you use inside of decorators, depending on proposed "solution", might get hoisted up too, and thus break execution flow, or prevent function from hoisting, and thus break backwards compatibility. See Sebastian McKenzie's answer here for a complete example and explanation. (This is not an issue for classes since those are specified as non-hoistable)

Now, let's switch to a more interesting question: how can we workaround this?

Workaround #1 (just call it)

Well, first of all, decorators are just function calls. That is, whenever you do

@decorator
class A {}

that is equivalent of

class A {}
A = decorator(A) || A;

So, apart from || part, imaginable decorator for funtions

@decorator
function f() {}

could be written in old good JavaScript as simply as

var f = decorator(function f() {});

(note that this also prevents ambiguity caused by hoisting since in this case variable will be hoisted, but expression won't)

The case where it makes a difference is when you have a bunch of decorators that you want to compose onto the original definition (class, method, function). No-one really wants to write things like

var f = ...(((((...reportErrors(customReporter, measureTime(debounce(function f() {}, 1000)))...)))))...; // whatever

Workaround #2 (runtime - boring but works)

Just use some fancy runtime API that would allow you to pass a bunch of decorators that would be wrapped around your function. For example, Lodash can definitely do this, and I'm pretty sure there is a lot of others around. Again, this works in old good pre-ES6 JavaScript.

Workaround #3 (hacky but with true decorators)

Since decorators can be defined on object methods, we can use a simple trick: define decorator on the method and then extract the function back:

function debounce(time) {
  return function (target, key, desc) {
    var fn = desc.value,
      lastStart = 0;
    desc.value = function () {
      var now = Date.now();
      if (now - lastStart >= time) {
        lastStart = now;
        return fn.apply(this, arguments);
      }
    };
  };
}

function measureTime(target, key, desc) {
  var fn = desc.value,
    { name } = fn;
  desc.value = function () {
    console.time(name);
    var result = fn.apply(this, arguments);
    console.timeEnd(name);
    return result;
  };
}

var myFn = {
  @debounce(200)
  @measureTime
  func(n) {
    console.log(`myFn(${n})`);
  },
}.func;

myFn(1);
myFn(2);
myFn(3);

setTimeout(() => myFn(4), 500);

See it in action in Babel REPL

Workaround #4 (syntactic and my favourite)

So, what we can actually do here, is use another composable operator similar to pipe operator from functional languages (F#, Elixir etc.) - it's a bind operator (NOTE: this operator wasn't proposed to TC39 yet and is implemented by Babel purely for experimental and feedback purposes)

Here's how it works when used as a function call:

context::fn1("a")::fn2("b")::fn3("c");

turns into

fn3.call(fn2.call(fn1.call(context, "a"), "b"), "c");

Beautiful, isn't it?

So, why don't we just put original function itself as a context for operator? In that case, we can decorate it with as much decorators as we wish in a truly composable way!

Here's a simple example of how such pseudo-decorators can be implemented and used:

function debounce(time) {
  var fn = this,
    lastStart = 0;
  return function () {
    var now = Date.now();
    if (now - lastStart >= time) {
      lastStart = now;
      return fn.apply(this, arguments);
    }
  };
}

function measureTime() {
  var fn = this,
    { name } = this;
  return function () {
    console.time(name);
    var result = fn.apply(this, arguments);
    console.timeEnd(name);
    return result;
  };
}

var myFn = ((n) => console.log(`myFn(${n})`))::measureTime()::debounce(200);

myFn(1);
myFn(2);
myFn(3);

setTimeout(() => myFn(4), 500);

See it in action in Babel REPL

UPD: On Twitter I've got a response with another beautiful example of decorator that can be used with :: syntax - and it's proper currying in JavaScript!


That's it! Feel free to ask any questions in comments.


More posts: