daniel@brockman.se
A general solution to the simple but common problem of attaching event listeners to HTML elements owned by object-oriented code is presented. The solution does not rely on the questionable method of injecting hidden backreferences to the application logic into the HTML elements; rather, the listeners are connected to the application logic in a purely hygeinic fashion through the use of lexical closures.
The following code does not do what the programmer intended it to:
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting")); this.element.onclick = this.greet; } GreetingButton.prototype.greet = function () { alert(this.greeting); }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
Upon loading the document, the expression gb.greet()
will be
evaluated, which will produce the greeting Hello!. However,
upon clicking the button labeled Receive Greeting, the
string undefined will appear instead of Hello!.
The reason is that in the latter case, this
in the expression
alert(this.greeting)
will refer to the HTML
button that was clicked—not, as in the former case, gb
, the
GreetingButton
object.
When we say gb.greet
, we get the exact same function
object as we get by saying GreetingButton.prototype.greet
—a
function object that obviously has no way of knowing that, in this case,
we would like to think of it as a method of the object gb
.
In JavaScript, the exact same function object may act as a method for any
number of different objects: the relevant object is supplied by the caller
in each case and bound to the special variable this
.
Consider what would happen if we created another greeting button—let's
call it foo
. Using foo
, we would have another
way of referring to the same old function object: foo.greet
.
The expressions gb.greet
and foo.greet
would be
entirely equivalent.
With this background, one immediately realizes that the line
this.onclick = this.greet
cannot do the right thing,
because it is equivalent to this.onclick = foo.greet
.
Let me remark also that although the expressions gb.greet
and foo.greet
are equivalent, the similar expressions
gb.greet()
and foo.greet()
are not.
To side-step the problem, you might consider doing this:
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting")); this.element.greetingButton = this; this.element.onclick = this.greet; } GreetingButton.prototype.greet = function () { alert(this.greetingButton.greeting); }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
This code works as intended, but the solution is inelegant.
While it is unlikely that the name greetingButton
will ever conflict with anything, you’re still essentially
polluting someone else’s namespace.
Another problem with this approach is that it theoretically breaks encapsulation, since you are in fact setting up a backdoor to your logic. Anyone with access to the HTML document that your element is attached to could aquire a reference to your backdoored element, and, by extension, your private data. This will probably not present itself as a practical problem to anyone, but it necessarily remains a fundamental inelegancy.
Binding a variable to the value of this
and then
closing over it is a superior approach:
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting")); var greetingButton = this;this.element.onclick = this.greet;this.element.onclick = function () { greetingButton.greet(); }; } GreetingButton.prototype.greet = function () { alert(this.greeting); }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
Essentially, a closure, or lexical closure, is a function f coupled with a snapshot of its lexical environment (i.e., the non-local variable bindings used in its body).
Hence, closing over some variable v means creating a closure that refers to v.
Strictly, the term “closure” can be used to describe any function that
refers to one or more variables in an outer lexical scope. This is a
rather broad definition that includes, for example, all functions that
refer to global variables (such as document
,
alert
, String
, and so on).
Some closures are more worthy of the classification. Firstly, it is common to exclude functions whose only outer scope is the global scope. That is, global functions are usually not described as closures. Secondly, the interesting kind of closures are those that really utilize the power leveraged by lexical scoping. I try to use the term only when f has the following characteristics:
The canonical example of a closure is the counter:
function makeCounter(start) { var current = start; function counter() { return current++; }; return counter; }
Here, our f — the closure — is counter
,
which appears within the body of makeCounter
(our
g). The variable current
is bound by
makeCounter
and referred to in the body of
counter
. Finally, counter
can be called by
code that does not appear within the body of makeCounter
because it is returned to the outside code. In case you were wondering,
makeCounter
could equivalently be written like so:
function makeCounter(current) { return function () { return current++; }; }
It is important to understand that a new closure is created and
returned every time makeCounter
is called. The parallels to
object-oriented programming are obvious and indeed interesting. If you
are confused about closures and lexical scoping, I recommend the classic
Structure and Interpretation
of Computer Programs (they just call them procedures,
though—not closures).
You might wonder why we can’t just close over this
,
instead of declaring a proxy variable and closing over that.
var greetingButton = this;this.onclick = function () {greetingButton.greet();this.greet(); };
The reason why this approach won’t work is that this
is
always bound anew every time a function is called, so the outer binding
would be shadowed by a new binding of this
.
Refactoring this a bit, we get the following:
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting"));this.element.onclick = function () { greetingButton.greet(); };this.element.onclick = createMethodReference(this, "greet"); } GreetingButton.prototype.greet = function () { alert(this.greeting); }; function createMethodReference(object, methodName) { return function () { object[methodName](); }; }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
And thus we arrive at the essential point of this article. We have just discovered a basic method for “welding” a function and an object, forming what we have been calling a method reference.
I call this form of event listening “object-oriented,” because instead of attaching callback functions, you attach callback methods. When doing object-oriented programming, an equivalent or similar facility is nearly essential.
I say “through partial application” because partial
application—i.e., informally, calling a function with only a
subset of its arguments and getting back the remainder—is actually
what createMethodReference
does.
The concept of partial application can be visualized by thinking of a function as a box with a number of empty slots—one for each argument that the function expects. Partially applying the function to some arguments, then, amounts to plugging just those arguments into the appropriate slots, producing a new box with fewer empty slots.
Now note that a method can be thought of as a function taking an extra
first argument (i.e., the new value of this
). Thus, when we
call createMethodReference(o, "m")
, we are just plugging
o
into the first slot (or, perhaps more accurately, the
“zeroth” slot) of the function o.m
—partial application.
When you click a button on a web page, the browser calls the event
listener referred to by the element’s onclick
property.
It sends one argument to the event listener: the event object,
which says things like the exact coordinates at which the click occured.
However, using the above definition of
createMethodReference
, an event listener method has no way of
accessing the event object. Therefore, we would like the method reference
to pass on whatever arguments it receives—the event object, in this case—to
the method it references.
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting")); this.element.onclick = createMethodReference(this, "greet"); } GreetingButton.prototype.greet = function (event) { alert(this.greeting); }; function createMethodReference(object, methodName) { return function () {object[methodName]();object[methodName].apply(object, arguments); }; }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
The apply
method on functions is analogous to the
apply
functions found in Common Lisp and Scheme, except that
the this
argument must be given separately.
In pseudocode, f.apply(o, as)
means roughly this:
o.___tmp___ = f; o.___tmp___(as[0], as[1], as[2], ...);
It might help to note that o.m.apply(o, [a, b, c])
is exactly equal to o.m(a, b, c)
.
Note that arguments
in the expression
apply(object, arguments)
refers to the arguments of
the inner, anonymous function—not the
createMethodReference
function itself.
But wait. In making the greet
method take an event object,
we have introduced a subtle inconsistency: our code is explicitly calling
the greet
method without an event object. We’ll just add some
indirection to clean this up:
function GreetingButton(greeting) { this.greeting = greeting; this.element = document.createElement("button"); this.element.appendChild(document.createTextNode("Receive Greeting"));this.element.onclick = createMethodReference(this, "greet");this.element.onclick = createMethodReference(this, "buttonClicked"); } GreetingButton.prototype.buttonClicked = function (event) { this.greet(); }; GreetingButton.prototype.greet = function (event) { alert(this.greeting); }; function createMethodReference(object, methodName) { return function () { object[methodName].apply(object, arguments); }; }; onload = function () { gb = new GreetingButton("Hello!"); document.body.appendChild(gb.element); gb.greet(); };
eval
You might want to be able to pass any function to
createMethodReference
instead of just a method name.
That way, you can create method references to functions which
aren’t really “methods” of the object in question.
function createMethodReference(object, methodName) { if (!(method instanceof Function)) method = object[method]; return function () { method.apply(object, arguments); }; };
But perhaps more convincing is the fact that we no longer have to deal with strings whose content are more or less code:
this.element.onclick = createMethodReference(this, "buttonClicked");this.element.onclick = createMethodReference(this, this.buttonClicked);
As of 2005, if you want to be compatible with Internet Explorer, you usually have to put this code at the top of every event listener:
event = event || window.event;
To avoid always having to do that, you can pull this logic into the function that creates the event listener method references:
function createEventListenerMethodReference(object, methodName) { return function (event) { object[methodName].call(object, event || window.event); }; }
In this case we assume that the method always takes exactly one argument,
so we do not have to use apply
to pass the arguments.
The call
method is just like apply
,
except that instead of taking an array of arguments, it simply takes the
actual arguments one after another. Thus, f.call(o, a, b, c)
is exactly the same as f.apply(o, [a, b, c])
.
The name createMethodReference
is clearly long and
awkward, so let’s rename our function to bind
and put
it on the prototype of Function
.
Function.prototype.bind = function (object) { var method = this; return function () { method.apply(object, arguments); }; }
Note again how we had to create a variable binding in order to close
over the value of this
. As previously stated, every function
implictly binds this
, so the above code works like this:
Function.prototype.bind = function (object) { var this = <object 1>; var method = this; return function () { var this = <object 2>; method.apply(object, arguments); }; }
Clearly, it wouldn’t do the same thing if it looked like this:
Function.prototype.bind = function (object) { var this = <object 1>;var method = this;return function () { var this = <object 2>;method.apply(object, arguments);this.apply(object, arguments); }; }
Now we can use it like this:
this.element.onclick = createMethodReference(this, this.buttonClicked);this.element.onclick = this.buttonClicked.bind(this);
Of course, you can do the same with the Internet Explorer-compatible event listener method reference creator:
Function.prototype.bindAsEventListener = function (object) { var method = this; return function (event) { method.call(object, event || window.event); }; }
We can already do partial application on the this
argument,
but we can just as easily allow for more general partial application:
// Most implementations don’t like invocations such as foo.concat(arguments) // or arguments.slice(1), due to a kind of reverse duck typing: an argument // object looks like a duck and walks like a duck, but it isn’t really a // duck and it won’t quack like one. function toArray(pseudoArray) { var result = []; // This is our real duck. for (var i = 0; i < pseudoArray.length; i++) result.push(pseudoArray[i]); return result; } Function.prototype.bind = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function () {method.apply(object, arguments);var newArguments = toArray(arguments); method.apply(object, oldArguments.concat(newArguments)); }; }
With this definition of bind
, a call such as f.bind(o,
1, 2)(3, 4)
will be rendered as f.call(o, 1, 2, 3, 4)
.
The corresponding redefinition of bindAsEventListener
Function.prototype.bindAsEventListener = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function (event) {method.call(object, event || window.event);method.apply(object, [event || window.event].concat(oldArguments)); }; }
will cause f.bindAsEventListener(o, "moomin")("snufkin")
to be rendered as f.call(o, event, "moomin", "snufkin")
.
This is handy whenever you want to attach an event listener in a
certain “mode”. For example, you might have a grid of cells, each
of which should react to click events.
function GridWidget(width, height) { // ...create elements and populate cell array here... for (var x = 0; x < width; x++) for (var y = 0; y < height; y++) cells[x][y].element.onclick = this.cellClicked.bindAsEventListener(this); } GridWidget.prototype.cellClicked = function (event) { alert("I have no idea which cell you just clicked."); };
As the message says, it is not obvious how to find out which one of
the width
× height
different cells generated the
click event, because all cells trigger the same event listener.
Certainly it would be ridiculous to attempt to manually define a separate
event listener function for each cell, especially since the number of cells
may even be variable.
The solution, of course, is declaring the event listener to take two additional arguments giving the cell coordinates, and using partial application to plug these values in during object initialization.
function GridWidget(width, height) { // ...create elements and populate cell array here... for (var x = 0; x < width; x++) for (var y = 0; y < height; y++) cells[x][y].element.onclick = this.cellClicked.bindAsEventListener(this, x, y); } GridWidget.prototype.cellClicked = function (event, x, y) {alert("I have no idea which cell you just clicked.");alert("You clicked the cell at (" + x + ", " + y + ")!"); this.cells[x][y].frobnicate(); };
It should not be hard to guess that it is useful to make the method references pass on any values that the real methods return.
Function.prototype.bind = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function () { var newArguments = toArray(arguments); return method.apply(object, oldArguments.concat(newArguments)); }; } Function.prototype.bindAsEventListener = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function (event) { return method.apply(object, [event || window.event].concat(oldArguments)); }; }
For example, it is often necessary to return false
from an
event listener to signal that the default action should be suppressed.
Finally, here is a way to enable destruction of method references:
var destructMethodReference = new Object; Function.prototype.bind = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function (argument) { if (argument == destructMethodReference) { method = null; oldArguments = null; } else if (method == null) { throw "Attempt to invoke destructed method reference."; } else { var newArguments = toArray(arguments); return method.apply(object, oldArguments.concat(newArguments)); } }; }
Now, to destruct a method reference, just invoke it with the special
object destructMethodReference
as argument. Being able to
destruct method references is useful when working around the Internet
Explorer reference cycle memory leak.
There is a bug in Internet Explorer that prevents its garbage
collector from reclaiming objects that are part of cyclic reference
chains which run through at least one DOM object (i.e., element).
The GridWidget
code above creates creates one such
reference cycle for each grid cell. Specifically, the cycle is
constructed as follows:
GridWidget
object;GridWidget
object has one reference to each
cell element.Since cell elements are DOM objects, Internet Explorer fails to reclaim
this cycle of objects and a memory leak results. The solution to this
problem is to break the cycles when you no longer need them. You can do
this in the onunload
event handler.
By the way, if you don’t like the global helper function
toArray
, just make it a local helper function:
(function () { function toArray(pseudoArray) { var result = []; for (var i = 0; i < pseudoArray.length; i++) result.push(pseudoArray[i]); return result; } Function.prototype.bind = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function () { var newArguments = toArray(arguments); return method.apply(object, oldArguments.concat(newArguments)); }; } Function.prototype.bindAsEventListener = function (object) { var method = this; var oldArguments = toArray(arguments).slice(1); return function (event) { return method.apply(object, [event || window.event].concat(oldArguments)); }; } })();
I want to thank the following people for their help in writing and revising this article.
My dear friend Peter Wängelin provided the initial motivation for the article by asking some good questions that I felt deserved some good, written answers.
Sam Stephenson promoted the article by linking to it from
his website and suggested to
people that they read it. He also put the bind
method
into his JavaScript library,
Prototype,
which is now bundled with
Ruby on Rails.
Very cool!
Mathieu van Loon corrected an error in the definitions of
bind
and bindAsEventListener
, and suggested that
method references should pass on the return values of the real methods.
Chih-Chao Lam pointed out the fact that argument objects cannot portably be used as arrays.
Ken Tozier asked whether one could avoid defining the helper
function toArray
globally.
Gary Hall, as well as a few other people, pointed out that Internet Explorer has a problem with cyclic references through DOM objects.
Jordan Gray spotted an error in bindAsEventListener
.
I’d also like to thank the likes of Anders Blomgren, Richard Davies, Chad Burggraf and Nick Eby, who were kind enough to mail me and thank me for writing the article.
Copyright © 2004, 2005, 2006, 2007, 2008, 2012 Daniel Brockman.
Permission is granted to copy, distribute and/or modify this
document under the terms of the GNU Free
Documentation License, Version 1.2 or any later version published by the
Free Software Foundation; with no Invariant Sections, no Front-Cover Texts,
and no Back-Cover Texts. Copies of the GNU Free
Documentation License can be obtained at the
following URL:
http://www.gnu.org/licenses/
In addition, I hereby place all the JavaScript source code in this article into the public domain. If that is not possible, anyone is permitted to use it for any purpose.