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.
8 comments:
What I proposed on bug 40080is a dirty hack. It helps a very little with debugging, but doesn't fix other problems you have mentioned.
Instead of
node.addEventListener("click", obj.callback.bind(obj))
I usually do
node.addEventListener("click", function(){
obj.callback();
});
An example:
var obj = {
func: function(){console.log(this.n)},
n: 1
}
document.addEventListener("click", function clicked(){
obj.func()
}, true)
I don't see any benefits in your proposal. What it does, what my version above can't do? Am I missing something?
Interesting thoughts, Patrick.
I have few comments:
1) Saying that `bind` gets in a way of debugging is similar to saying that anonymous functions get in a way of debugging. This is a valid argument of course, but it's something debuggers/profilers can relatively easy take care of. As you might know, `Function.prototype.toString` is specified to return implementation-dependent representation of a function (in both ES3 and ES5); Nothing is stopping implementations from returning source of original function prepended with something like [Bound function] and maybe also [Target function: ...], [Bound arguments: ...], etc.
2) I don't understand the "garbage" argument. I'm not sure why you think that closure is formed on creation of bound function. There is no change in scope of the function that gets bound to an object (and/or arguments). ES5 even explicitly states that bound function — contrary to regular native function objects — doesn't have [[Scope]] (internal property which is a reference to a lexical environment in ES5 and scope chain (list of activation objects) in ES3). In ES5 `bind` merely creates a wrapper object (that's made to look almost like a function object). This wrapper delegates invocation (`()` operator, internal [[Call]] method), instantiation (`new` operator, internal [[Construct]] method) and property existence check (`in` operator, internal [[HasInstance]] method) to an original (target) function. No change of [[Scope]] of target function is ever made and no [[Scope]] is ever created on a bound function.
Now compare this to a popular replacement of `bind` — creating a wrapping function:
el.addEventListener('click', obj.method.bind(obj), false);
el.addEventListener('click', function(){ obj.method(); }, false);
In a second example, wrapping function DOES in fact have a scope and a closure DOES get formed. Now, speaking of garbage, imagine if this second snippet was buried somewhere deep inside bunch of other code and how much garbage handler function would close over (pretty common case, imo):
(function(){
// ... some code and variables
(function(){
// ... some more code and variables
el.addEventListener('click', function(){
// all the variables from the enclosing scope
// are now closed over in this function
obj.method();
}, false);
})();
})();
When compared to wrapping-function bind replacement, I would think that `bind` makes for a more performant code, not the other way around.
3) Longish to type argument is a bit weak, in my opinion.
This is how we used to do things:
el.addEventListener('click', function(){ obj.method() }, false);
And this is how we can do it with bind now:
el.addEventListener('click', obj.method.bind(obj), false);
`bind` one is shorter. And it avoids creation of closure too. It's a win/win :)
Of course, `el.addEventListener('click', obj, 'method', false)` would be even shorter, but then you end up with 4 arguments and that's already something begging for "too many arguments" type of refactoring :)
4) The point about `bind` retuning new function every time — and that being "bad thing" — is interesting. I can see how it's inconvenient in case of `addEventListener`/`removeEventListener` (although in context of browser scripting, it's recommended to avoid frequent removal of listeners in favor of delegation).
More stuff on eventHandler recently published on Ajaxian: http://ajaxian.com/archives/an-alternative-way-to-addeventlistener
NV, see Kangax's point #2.
Thanks for the "dirty hack" on bug 40080; I haven't had a chance to use it, but will soon. Have you found it to be helpful?
kangax,
1) bind being "anonymous"; see the previously mentioned bug report by NV, which modify's the toString() method of the bound function for Web Inspector's internal code (doesn't help anyone but Web Inspector developers). For debug purposes, it might be nice if there was a "standard" for the toString(), or even better, or if there were a way to introspect over these bound functions.
2) You caught me, I hadn't fully read and/or understood the spec. Not sure I understand all the ramifications of it yet. But you're right, no new scope is created by just creating the bound function. Even in the case of libraries (jQuery, Dojo, etc) implementations of bind(), the scope is going to be container to that of the bind() implementation, which shouldn't be leaky, but it's something.
Of course, you just know we'll start seeing:
el.addEventListener('click', (function(){
console.log("in a click handler: " + this)
}).bind(this), false);
which puts us back in leaky closure mode :-)
I finally caught up on JavaScriptCore's Bug 26382: [ES5] Implement Function.prototype.bind, and there's some interesting bits in there about the eventual implementation, for folks interested in this stuff. Most interesting was realizing the shortcuts they planned on making, which didn't seem to safe to me at the time, but makes more sense now.
I happen to be watching the bind() implementation.
3) 'longish' isn't a terribly strong argument, blame Alex for that one :-) DRY is nice though, as in not having to repeat obj twice in the "obj.method.bind(obj)". This would be somewhat offset if the common case for "obj" is "this", leaving us with "this.method.bind(this)" is at least something that you can cut+paste around safely, assuming that's the style you're using for your code.
4) w/r/t this is at least one type of garbage I was referring to - the newly returned function itself.
kangax,
1) It's not just about `toString`. Look at http://elv1s.ru/files/js/bind_vs_closure.png and http://elv1s.ru/files/js/bind_vs_closure.html
The bound function is pointing to the Function.prototype.bind, which isn't very helpful. For me, that's the main problem of prototype.js-like bind. I'm pretty sure native `bind` won't have this problem.
> The bound function is pointing to the Function.prototype.bind, which isn't very helpful. For me, that's the main problem of prototype.js-like bind. I'm pretty sure native `bind` won't have this problem.
Define "pointing to". All property accesses on the bound function delegate to the original function? I don't think that's what the spec says (my current understanding), and I'm not sure that's the right thing to do anyway.
I think I'd rather that bound functions have some new special property that points to the original function. In the spec, it's the [[TargetFunction]] internal property, but internal properties aren't "real".
> Define "pointing to".
I meant the link in Web Inspector ("bind_vs_closure.html:10" in my example) is pointing to the line number where the bound function was created.
> I think I'd rather that bound functions have some new special property that points to the original function. In the spec, it's the [[TargetFunction]] internal property, but internal properties aren't "real".
I suppose [[TargetFunction]] might be accessed within Web Inspector's back-end. I hope so.
Post a Comment