JavaScript decorator pattern

I don’t care how many other places this is on the net, file this one under “so I won’t need to look ever again.”

I needed to augment an existing JavaScript function by applying a decorator to it, where I needed to dynamically add special behavior for this one page.  Ideally, I could have bound to some event, but a decorator worked just as well.  Instead of doing some custom JavaScript for decorating this one method, I modified the Function prototype, to add two new methods, “before” and “after”:

Function.prototype.method = function(name, func) {
    this.prototype[name] = func;
    return this;
};

Function.method('before', function(beforeFunc) {
    var that = this;
    return function() {
        beforeFunc.apply(null, arguments);
        return that.apply(null, arguments);
    };
});

Function.method('after', function(afterFunc) {
    var that = this;
    return function() {
        var result = that.apply(null, arguments);
        afterFunc.apply(null, arguments);
        return result;
    };
});

Each decorator method captures the original method (in the “that” variable) and returns a new method that calls the original and new method passed in, all without modifying the original method.  With these new Function prototype methods, I can do some highly interesting code:

var test = function(name) { alert(name + ' is a punk.'); };

test('Nelson Muntz');

test = test.before(function(name) { alert(name + ' is a great guy.'); });

test('Milhouse');

test = test.after(function(name) { alert(name + ' has a fat head.'); });

test('Jimmy');

We see from this code, in succession, alerts:

  • “’Nelson Muntz is a punk”
  • “Milhouse is a great guy”
  • “Milhouse is a punk”
  • “Jimmy is a great guy”
  • “Jimmy is a punk”
  • “Jimmy has a fat head”

This code was brought to you by lessons learned from the awesome JavaScript book, “JavaScript: The Good Parts”, and the magic of closures.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in JavaScript. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://sergiopereira.com/blog Sergio Pereira

    Irony is unforgiving. BLOCKED SCRIPT describes very well how much we still have to go.

  • http://brianc.me/blog Brian C

    Might want to check out what happens when you do the following:

    var bang = function(){
    return{
    name:”Jimmy”,
    sayHello:function(){
    alert(“Hello from ” + this.name);
    }.before(function(){/*do nothing*/})
    }
    }()

    using “apply” with null as the first argument removes the “this” context of the original function, so any reference to “this” inside the function returned from “before” or “after” methods might result in some unhappy times.

  • http://brianc.me/blog Brian C

    two things:

    first…
    I should have actually invoked the offending function…in your minds eye, append this to the above code:
    bang.sayHello();
    word….

    second…
    You can modify your before and after functions to take a “scope” or “execution context” as their argument to optionally apply an execution context to the method…

    Function.method(‘before’, function(beforeFunc,scope) {
    var that = this;
    return function() {
    beforeFunc.apply(scope, arguments);
    return that.apply(scope, arguments);
    };
    });

    My first example’s invocation of “before” could be modified to as follows:

    }.before(function(){/*do nothing*/},this) and I believe the sayHello method would not cause an error….unfortunately it’s too late and I’m too lazy to test this out. So…I might be wrong.

    Either way, thanks to JS’s optional method parameters this modification is a completely backwards compatible change. :D

  • http://www.lostechies.com/members/bogardj/default.aspx bogardj

    @Brian C

    Ha, I knew I was forgetting something! I’ll check out that change, thanks!