Python-Style Decorators in JavaScript

Python-style decorators (or Java annotations) are a useful feature that is not natively available in JavaScript. This article describes a technique for apply Python-style decorators to JavaScript functions. We’ll call them annotations, so not to confuse them with the decorator pattern.

How do it…

To illustrate what an annotation will be, lets try a simple example:

function alertFoo() {
    alert('foo');
}
alertFoo = annotate(alertFoo).by(alertBar);

We annotate the alertFoo function with the alertBar function (not yet defined). This is equivalent to executing the following code:

alertFoo = alertBar(alertFoo);

Annotations are a form of metaprogramming; they enhance/change the action of the function or method they annotate. For example, if we define the alertBar annotation:

function annotationBar(fnAnnotated) {
	return function() {
		alert("bar");
	}
}

Then the annotated alertFoo will alert bar instead of foo. Go ahead and give it a try:

Unannotated alertFoo
Annotated alertFoo

We replaced the behavior of alertFoo using an annotation. That’s useful, but not terribly powerful.

Perhaps, the annotation should check something, hijacking the function if some variable isn’t met, but executing the annotated function normally otherwise. The following function will alert bar when the click counter is odd, but delegate to the annotated function, alerting foo when the counter is even:

function annotationBarWhenTrue(fnAnnotated, fnEval) {
	return function() {
	    if (fnEval()) {
	        alert("bar");
	    }
	    else {
		    fnAnnotated.apply(this, arguments);
		}
	}
}
var iCallCounter = 0;
alertFoo = annotate(alertFoo).by(alertBar, function() {
    return iCallCounter++ % 2; // true when odd
});

Alert bar when click count is odd, foo otherwise

As you can see arguments may be passed when annotating a function into the annotation itself and used for evaluation. Additionally, the annotated function may be called directly inside the inner annotating function, allowing for annotation chaining.

The last thing to show is annotation chaining. Below we chain two additional alerts onto alertFoo:

function annotationBar(fnAnnotated) {
    return function() {
        alert("bar");
        fnAnnotated.apply(this, arguments);
    }
}

function annotationBaz(fnAnnotated) {
    return function() {
        alert("baz");
        fnAnnotated.apply(this, arguments);
    }
}
alertFoo = annotate(alertFoo).by(annotationBar).by(annotationBaz);
alertFoo();

Alert baz, bar, then foo using chained annotations

Once a function is annotated, calling annotate or by functions will continue to use the existing annotation chain, so the above could be rewritten as:

annotate(alertFoo); // adds alertFoo.by
alertFoo = alertFoo.by(annotationBar);
alertFoo = annotate(alertFoo).by(annotationBaz);
alertFoo();

Because the annotate function be improved over time, I have not included the source-code here. You can view the latest version at annotation.js.

How it works…

Annotation.js creates a function called annotate that can be used to annotate existing JavaScript functions. By default annotate will return the function passed in as its only argument, but augment it with a by function that can be used to add annotations. This allows the function calls to use natural language when annotating: annotate(functionToAnnotate).by(myAnnotatingFunction);.

The by function supports chaining and returns the the annotated original function (or the last one on the annotation chain, if not the original function). It accepts any number of arguments, but the first argument must be the annotating function; all other arguments will be passed to the annotating function to create the annotation wrapping function. The annotating functions should return a function which is what will be executed when the annotated function is called.

Because we are using JavaScript simply calling the annotate function doesn’t update the original function with the annotated version. As shown in all the examples above, you must assign the response of the annotate and by functions to the original variable.

Annotations can be used to replace the behavior of a function, augment the behavior of a function, and/or perform other complex operations. Although, not shown above, they are particularly useful, when you want to override the behavior of a single instance of an object, instead of all instances, as might be done by changing the prototype.

There’s more…

The section above describes how to use the annotate function, below will go into details of how it works. To begin, a function is passed into annotate that you want add annotations to. It first checks to see if the function is already annotated by looking for the special by function. This ensures that only one annotation chain exists, and reduces confusion and corner-cases in complex applications. If the function wasn’t previously annotated we initialize the annotation chain with the function to annotate. The annotation chain is used to maintain the call stack of annotations.

Lastly, the by function is added to the function to annotate and the function to annotate is returned. At this point no annotations have been made, but the returned function is in an annotation aware state with the by function available. The original function to annotate variable has been modified with the by function, so you do not necessarily need to chain functions, but I think the code is more understandable when you do.

The by function is where the annotation magic happens. We first find the function that should be annotated (wrapped by our annotation logic) by looking at the annotation chain. The last item in the chain, is the one that should be annotated next. Initially, this will be the original function to annotate, but will become a stack of each subsequent annotation. We then use a wrapping function to call the annotating function passing the function to annotate and all other arguments that were passed into the by function. The wrapping function is used to allow modifying the arguments array without changing the function that the variable fnAnnotation points to. Thus I can call fnAnnotation using the apply method without needing to enumerate the arguments.

The annotating functions are special functions that should accept the function it annotates and any additional arguments that are expected to be passed into the by function. They should return an inner function that does whatever the annotation is expected to do. If the annotation should chain to the function it annotates, simply call fnAnnotated.apply(this, arguments); from the inner function.

The last part of the by function pushes the function returned by the annotation function onto the annotation chain, augments it with the by function, and returns it. This is what allows the chaining of the by functions.

I find the annotate function intuitive and easy to use, even though explaining it is not, and I hope you do as well. That said, there are two features that I have not yet implemented (but hope to eventually): objects cannot yet be annotated as they can in Python 2.7+, and there is not unannotated function yet. Please leave a comment, describing how you are using annotations in your codebase.

Additional Reading

The annotate function tries to emulate Decorators in Python, so you may find the Wikipedia article helpful.