Nick Desaulniers

The enemy's gate is down

Function.prototype.bind Edge Cases

| Comments

ECMAScript 5’s Function.prototype.bind is a great tool that’s implemented in all modern browser JavaScript engines. It allows you to modify the context, this, of a function when it is evaluated in the future. Knowing what this refers to in various contexts is key to being a professional JavaScript developer; don’t show up to an interview without knowing all about it.

Here’s a common use case that developers need to watch for. Can you spot the mistake?

1
2
3
4
5
6
7
8
9
10
var person = "Bill";

var obj = {
  person: "Nick",
  hi: function () {
    console.log("Hi " + this.person);
  }
};

window.addEventListener("DOMContentLoaded", obj.hi);

Ooops!!! Turns out that since we added the event listener to the window object, this in the event handler or callback refers to window. So this code prints "Hi Bill" instead of "Hi Nick". We could wrap obj.hi in an anonymous function:

1
2
3
window.addEventListener("DOMContentLoaded", function () {
  obj.hi();
});

But that is so needlessly verbose and what we were trying to avoid in the first place. The three functions you should know for modifying this (a question I ask all my interview candidates) are Function.prototype.call, Function.prototype.apply, and Function.prototype.bind. call is variadic, while apply takes an array of arguments, but the two both immediately invoke the function. We don’t want to do that just yet. The fix we need is Function.prototype.bind.

1
window.addEventListener("DOMContentLoaded", obj.hi.bind(obj));

There, now isn’t that nice and short? Instead of saving this as another variable then closing over it, you can instead use bind!

1
2
3
4
5
6
7
8
9
var obj = {
  person: "Nick",
  wait: function () {
    var self = this;
    someButton.onclick = function () {
      console.log(self.person + " clicked!");
    };
  },
};

becomes

1
2
3
4
5
6
7
8
var obj = {
  person: "Nick",
  wait: function () {
    someButton.onclick = function () {
      console.log(this.person + " clicked!");
    }.bind(this);
  },
};

No need to store this into self, then close over it. One great shortcut I use all the time is creating an alias for document.getElementById.

1
2
3
4
5
var $ = document.getElementById.bind(document);
$('someElementsId').doSomething();
$('anotherElement').doSomethingElse();
$('aThirdElement').doSomethingDifferent();
$('theFifthElementOops').doSomethingFun();

Why did I bind getElementById back to document? Try it without the call to bind. Any luck?

bind can also be great for partially applying functions, too.

1
2
3
4
5
6
7
function add (a, b) {
  console.log("a: " + a);
  console.log("b: " + b);
  return a + b;
};
var todo = add.bind(null, 4);
console.log(todo(7));

will print

1
2
3
a: 4
b: 7
11

What Function.prototype.bind is essentially doing is wrapping add in a function that essentially looks like:

1
2
3
var todo = function () {
  add.apply(null, [4].concat(Array.prototype.slice.call(arguments)));
};

The array has the captured arguments (just 4), and is converting todo’s arguments into an array (a common idiom for converting “Array-like” objects into Arrays), then joining (concat) them and invoking the bound function (apply) with the value for this (in this case, null).

In fact, if you look at the compatibility section of the MDN page for bind, you’ll see a function that returns a function that is essentially the above. One caveat is that this approach only allows you to partially apply variables in order.

So bind is a great addition to the language. Now to the point I wanted to make; there are edge cases when bind doesn’t work or might trip you up. The first is that bind evaluates its arguments when bound, not when invoked. The other is that bind returns a new function, always. And the final is to be careful binding to variadic functions when you don’t intend to use all of the passed in variables. Um, duh right? Well, let me show you three examples that have bitten me (recently). The first is with ajax calls.

1
2
3
4
5
6
7
8
function crunch (data) {
  // operate on data
};

var xhr = new XMLHttpRequest;
xhr.open("GET", "data.json");
xhr.onload = crunch.bind(this.response);
xhr.send();

Oops, while I do want to operate on this.result within crunch with this referring to xhr, this at the time of biding was referring to window! Let’s hope window.results is undefined! What if we changed this.result with xhr.result? Well, we’re no longer referring to the window object, but xhr.result is evaluated at bind time (and for an unsent XMLHttpRequest object, is null), so we’ve bound null as the first argument. We must delay the handling of xhr.onload; either use an anonymous function inline or named function to control nesting depth.

1
2
3
xhr.onload = function () {
  crunch(this.result);
};

The next is that bind always returns a new function. Dude, it says that in the docs, RTFM. Yeah I know, but this case still caught me. When removing an event listener, you need to supply the same handler function. Example, a once function:

1
2
3
4
5
6
function todo () {
  document.removeEventListener("myCustomEvent", todo);
  console.log(this.person);
});

document.addEventListener("myCustomEvent", todo.bind({ person: "Nick" }));

Try firing myCustomEvent twice, see what happens! "Nick" is logged twice. A once function that handles two separate events is not very good. In fact, it will continue to handle events, since document does not have todo as an event handler for myCustomEvent events. The event listener you bound was a new function; bind always returns a new function. The solution:

1
2
3
4
5
var todo = function () {
  console.log(this.person);
  document.removeEventListener("myCustomEvent", todo);
}.bind({ person: "Nick" });
document.addEventListener("myCustomEvent", todo);

That would be a good interview question. The final gotcha is with functions that are variadic. Extending one of my earlier examples:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
  person: "Nick",
  wait: function () {
    var someButton = document.createElement("button");
    someButton.onclick = function () {
      console.log(this.person + " clicked!");
    }.bind(this);
    someButton.click();
  },
};
obj.wait();

Let’s say I thought I could use bind to simplify the onclick using the trick I did with document.getElementById:

1
2
3
4
5
6
7
8
9
var obj = {
  person: "Nick",
  wait: function () {
    var someButton = document.createElement("button");
    someButton.onclick = console.log.bind(console, this.person + " clicked!");
    someButton.click();
  },
};
obj.wait();

Can you guess what this prints? It does prints the expected, but with an unexpected addition. Think about what I said about variadic functions. What might be wrong here?

Turns out this prints "Nick clicked! [object MouseEvent]" This one took me a while to think through, but luckily I had other experiences with bind that helped me understand why this occurred.

console.log is variadic, so it prints all of its arguments. When we called bind on console.log, we set the onclick handler to be a new function that applied that expected output with any additional arguments. Well, onclick handlers are passed a MouseEvent object (think e.target), which winds up being passed as the second argument to console.log. If this was the example with add from earlier, this.person + " clicked!" would be the 4 and the MouseEvent would be the 7:

1
2
3
someButton.onclick = function (e) {
  console.log.apply(console, ["Nick clicked!"].concat([e]));
};

I love bind, but sometimes, it will get you. What are some examples of times when you’ve been bitten by bind?

Comments