A recent passive-aggressive twitter message from me:
Oh yeah. Keep on bind()'ing folks, because bind() rocks. http://bit.ly/aPyfuV
In case you can't tell, the message is facetious. bind()
does not actually rock.
what is bind()
?
bind()
is a JavaScript function implemented by many JavaScript libraries and included in ECMAScript 5. If you aren't already familiar with bind()
, this post isn't going to make much sense, but if you're curious anyway, see Prototype's documentation for their bind()
.
Let's look at what bind()
is actually doing:
- associates an object to use as
this
(aka the receiver) when the function is invoked - associates additional parameters to be passed to the function when the function is invoked (aka currying).
bind()
does this by returning a new function, which when invoked, arranges for the object you want as the receiver and the additional parameters to be passed to the original function. It hides these values in the newly created function, associated with the closure the function was returned from.
bind()
is frequently used for callbacks and event handlers, which typically allow you to pass a function as the callback, but don't provide a way to specify the receiver or additional parameters for the actual call to the callback.
why does bind()
not rock?
gets in the way of debugging
Take a look at the bug I linked to in the Twitter message above: Bug 40080: Web Inspector: better Function.prototype.bind for the internal code
The bug concerns providing a better debug story around functions that have been
bind()
'd. Because the current story isn't very pretty. When you stumble upon abind()
'd function in the debugger, you see the source for thebind()
function itself, which isn't what you want to see. In addition the resulting bound function is usually anonymous, which means your stack traces, profile reports, etc, will be filled with (anonymous function) entries.garbage
Calling
bind()
creates a new function object every time it's called. Not just a string, or empty object, or array. A function. Which I'm guessing is more expensive than simpler objects. [yes, I should do some measurements.] A closure is also created and associated with the function (which is where the receiver and curried arguments are stored). More garbage.Now imagine that, for whatever reason, you need to add and then remove a callback frequently, over and over again, for some reason. Or set a timer over and over again. And you're using
bind()
. Think of the garbage you're creating.Alex Russell posted some thoughts on
bind()
in a post to the es-discuss mailing list (item 3).So why does this [using
bind()
] suck? Two reasons: it's long-ish to type, and it doesn't do what the dot operator does -- i.e., return the same function object every time.It is longer, and therefore yuckier. It's also often not DRY (
obj
referenced twice):setTimeout( obj.method.bind(obj, "John"), 100 );
The second point is explained by Alex in his post:
Many functions, both in the DOM and in the library, accept functions as arguments. ES5 provides a bind() method that can ease the use of these functions when the passed method comes from an object. This is, however, inconvenient. E.g.:
node.addEventListener("click", obj.method.bind(obj, ...));
Also, insufficient. Event listeners can't be detached when using Function.prototype.bind:// doesn't do what it looks like it should node.removeEventListener("click", obj.method.bind(obj, ...));
So the trick here, if you want to use
removeEventListener()
, is that you have to store the result of a single call tobind()
and then use that value in subsequentaddEventListener()
/removeEventListener()
paired calls. Alex's post suggests a new language feature to work around this (btw, I'm not in favor of his proposed language feature).
why are we in a bind with bind()
today?
It's pretty obvious to see how we got to the point where you need to use bind()
in your code today.
Historically, JavaScript was a glue language that let you do a light amount of programmatic processing against stuff in your page. When specifying a callback/listener, you didn't have to worry so much about the receiver of the function you passed in; you were probably using global variables instead of creating your own little objects.
And so the places that take callbacks, like setTimeout()
,
onload
handlers, etc, didn't really have a need for you to specify the receiver of of the callback when it was invoked. The receiver was always ... well, whatever it was for your callback.
Fast-forward a decade, and now we have people building huge systems out of JavaScript, using some sort of "class" story, or living the hippy prototype lifestyle, or who knows what kids are doing today. In any case, there's often an "object" in the picture, and you'd often like to arrange to have that object be the receiver of the callback. Quite often, you'd like for the callback function to be a method of an object, and have the receiver of the callback be that object.
The problem is, there's no where to put the receiver; all the pre-existing callback patterns just allowed the use of a function parameter. The trick with bind()
is that it attaches the receiver, and possibly curry'd arguments, to an invisible bag wrapped around a newly created function which is a delegated version of your callback function. Nature will find a way.
how can we fix the evils of bind()
?
For me, the root of the problem is that we're passing the method receiver in a secondary channel, the bound function. So, stop doing that. Pass it explicitly.
Let's play with changing the addEventListener()
function to accommodate a new receiver
parameter. Here's the current function signature:
target.addEventListener(type, listener, useCapture)
We can add the receiver
parameter to the end of the function:
target.addEventListener(type, listener, useCapture, receiver)
or we could allow listener
and receiver
to be combined together in an array and used where the existing listener
value is today:
target.addEventListener(type, [receiver, listener], useCapture)
This second flavor tastes better to me.
what does the fix smell like?
Let's compare the code. For the examples below, obj
is the receiver of the callback, callback()
is a method available on the obj
object.
Using ECMAScript 5's bind()
method:
node.addEventListener("click", obj.callback.bind(obj))
Here's the four-arg addEventListener()
:
node.addEventListener("click", obj.callback, false, obj)
Here's the two-element-array-listener addEventListener()
:
node.addEventListener("click", [obj, obj.callback])
These examples could be made even DRYer, if instead of passing a function reference, you pass a string, which will be used as a property name to obtain the function from the receiver object:
node.addEventListener("click", "callback", false, obj) node.addEventListener("click", [obj, "callback"])
In terms of being able to handle the removeEventListener()
case as well, when using the ECMAScript 5 version of bind()
you would have to arrange to store a copy of the bound function, so you can send the exact same function both addEventListener()
and removeEventListener()
. My proposed versions could do a compare against the parameters or array elements, allowing you to use the exact same parameters on addEventListener()
and removeEventListener()
.
In other words, here's how you do it in ECMAScript 5:
var boundFunction = obj.callback.bind(obj) node.addEventListener("click", boundFunction) ... node.removeEventListener("click", boundFunction)
And here's how you do it with my proposal:
node.addEventListener("click", [obj, "callback"]) ... node.removeEventListener("click", [obj, "callback"])
This invocation pattern works the same for the other form of call that I proposed.
Curried arguments can be handled the same way the receiver
parameter is handled; passed as additional arguments to addEventListener()
(not needed for removeEventListener()
?), or an additional element in the array where the listener
argument was previously used. One simplification would be to allow a single curried argument - other callback systems typically refer to this as userData
or clientData
- rather than deal with a variadic list. It's simple enough to combine multiple curried arguments into an object or array for use as a userData
argument.
actually, bind()
isn't always evil
Although I've spent this entire post complaining about bind()
, I will acknowledge it's power and usefulness. Particularly in meta-programming and function programming.
My primary complaint is having to use bind()
is something pedestrian as callbacks. That's too much.
the obligatory Smalltalk reference
My views on this subject are biased by my exposure to OTI/IBM Smalltalk. Read up on the Callbacks section on page 150 of "IBM Smalltalk: Programmer's Reference".
You'll note that my suggestion here is no different than the addCallback:receiver:selector:clientData:
method described in that manual, just a shorter name.