JavaScript Binding

JavaScript’s this keyword has always given me a massive headache. Any JavaScript codebase is inevitably riddled with events and callback functions. This is nice for simple asynchronous web scripting, but when you start writing projects with complicated objects that handle events, it gets hairy.

Until today, the best and essentially only solution I had to date relied on jQuery’s event handling functions. This always involved passing an event a data object containing a self property which pointed to the object I actually wanted “this” to refer to. The code would look something like this:

var Foo = function () {
    this.text = "bar";
    this.button = $('#foo-button');
    this.button.on('click', null, {self: this}, function () {
        alert(self.text);
    });
}

But when we converted all of our widgets to use our new widget creation library, jQuery had to be scrapped. We’re writing pure JavaScript, not Addon SDK content scripts. Including jQuery was not feasible. So I scoured the Internet for some more portable solution. And in a startlingly short time, I found a solution so simple that  I felt very, very dumb.

That solution is the bind() function. All JavaScript functions have the bind method on their prototype, so this worked effortlessly in all of my module code. The syntax is pretty simple. For a function myBoundFunction() whose “this” you want to reassign to the object newThis, you’d make a call to myBoundFunction.bind(newThis). Simpler than the jQuery code, and much more portable. It’s everything I ever dreamed of and more!

Now let’s take a look at it working in the actual code base. The following snippets from the Bugzilla widget’s user.js defines some functions on the User object:

exports.User.prototype = {
    /**
     * Sets the document.
     *
     * @param {XULDocument} document The document in which the user's form exists.
     */
    setDocument: function (document) {
        // Set document
        this.document = document; 

        // Elements
        this.nameElement = this.document.getElementById("user-name");

        // Event handlers
        this.nameElement.addEventListener('change', this.onUserNameSubmit.bind(this), false);
    },
[some other code that's  not relevant to this example]
   /**
     * Username submission event handler.
     *
     * Shuts down the form submission and checks whether the given user is valid.
     */
    onUserNameSubmit: function (event) {
        // Get the user name from the form
        var userName = this.getUserName();

        // Is the user on the Bugzilla server?
        var userNameFound = this.queryUserName(userName);
    },

Inside of User.setDocument() we rig up the User’s single event handler. When a username is entered in the nameElement text box, the change event fires and User.onUserNameSubmit() is executed. In User.onUserNameSubmit(), we really want to refer to  functions that the User has. But unless we bind the function, we’ll only get access to the username entry text box element. By passing the event listener this.onUserNameSubmit.bind(this), we are setting all “this” in User.onUserNameSubmit() to the User object and can call other User member functions.

In addition to setting “this”, bind() allows us to pass additional arguments to pass to the target function. This is extremely useful for writing callback functions which a library or other code you can’t control will use. For example, the User object makes use of a bz.js call to get the number of bugs a user has on the Bugzilla server. The function bz.countBugs() takes a query object and a callback as arguments. The callback function is expected to take error and bugs as arguments, and bz.countBugs() will pass the callback an error object and a bug array when it calls the callback function. But if we want the callback to take a userName as well? bind() to the rescue!

    /**
     * Sends a query to the server to see whether the user exists.
     */
    queryUserName: function (userName) {
        // Start a waiting animation
        this.startSpinner();

        // Does the given user have bugs on the server?
        this.bz.countBugs({
            email1: userName,
            email1_assigned_to: 1,
            email1_qa_contact: 1,
            email1_type: "equals"
        },
        this.onUserNameResponse.bind(this, userName));
    },

    /**
     * Starts the spinner animation.
     */
    startSpinner: function () {
        this.nameElement.className = "spinner";
    },

    /**
     * Handles the response to the user name query.
     *
     * @param  {string} userName The username whose validity is being tested.
     * @param  {error} error Any errors with the request.
     * @param  {array} bugs An array of Bugzilla bug objects.
     */
    onUserNameResponse: function (userName, error, bugs) {
        // Stop the waiting animation
        this.stopSpinner();

        // Let the user know if the given userName had any bugs
        if (bugs > 0) {
            this.setUserName(userName);
        } else {
            this.rejectUserName();
        }
    },

Using bind() we set the “this” object and pass it an additional parameter, userName, to shove in front of the arguments passed in. So bz.countBugs() populates User.onUserNameResponse()’s  error and bugs parameters and the bind(this, userName) call injects the given userName as the first argument.

And that’s why bind() is the coolest thing I’ve learned about JavaScript to date.

Advertisements

~ by slenkeri on November 5, 2013.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: