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?
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:
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.
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!
var obj = {
person: "Nick",
wait: function () {
var self = this;
someButton.onclick = function () {
console.log(self.person + " clicked!");
};
},
};
becomes
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.
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.
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
a: 4
b: 7
11
What Function.prototype.bind is essentially doing is wrapping add in a
function that essentially looks like:
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.
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 binding 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.
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:
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:
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:
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:
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:
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?