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.