Friday, April 13, 2012

Removing anonymous event listeners in JavaScript

One of the features that I like about JavaScript is its support of closures. Here is a very simple example of closure in action:
function alertOnClick(message)
{
    var btn = document.getElementById('btnAlert');
    btn.addEventListener('click', function() {
        alert(message);
    }, false);
}

alertOnClick('You have clicked the button!');

The alertOnClick() function adds an anonymous event handler to a button that has been defined in the HTML page that runs the script. The event handler has no local variables of its own, but uses the 'message' variable declared in the outer function. This is an example of closure in JavaScript. The nested anonymous function has access to the arguments and variables of its containing function. In other words, the inner function contains the scope of the outer function. Note that the outer function cannot use the arguments and variables of the inner function.

Often I want an event listener to run only once, and remove it inside the listener function after it has been called. This is easy when the handler is not anonymous, but is defined as a global function instead:
var message = null;

function clickHandler()
{
    alert(message);
    // Remove the event listener, we no longer need it.
    // Note that 'this' refers to the event source: the button.
    this.removeEventListener('click', clickHandler, false);
}

function alertOnClick()
{
    var btn = document.getElementById('btnAlert');
    btn.addEventListener('click', clickHandler, false);
}

message = 'You have clicked the button!';
alertOnClick();

In this example removing the event listener is easy, but this way of implementing event listeners has a disadvantage. In order for the click handler to have access to a message variable, we must declare the variable globally. This is not very nice from a technical point of view.

Back to our first JavaScript example, that doesn't have this problem. In that example the event listener is an anonymous function. To remove such a listener within the listener function itself, we heed a way to get a reference to it. For that we will use the 'arguments' variable, that is available in every function automatically. It not only contains the arguments that are passed to a function but also a reference to the function itself: arguments.callee. We can use that reference to remove an anonymous event handler after it has been called.

Here is our first code example, with one additional line to remove the anonymous event handler:
function alertOnClick(message)
{
    var btn = document.getElementById('btnAlert');
    btn.addEventListener('click', function() {
        alert(message);
        // Remove the event listener, we no longer need it.
        // Note that 'this' refers to the event source: the button, and
        // arguments.callee contains a reference to the function itself.
        this.removeEventListener('click', arguments.callee, false);
    }, false);
}

alertOnClick('You have clicked the button!');

The 'arguments.callee' variable is rather useful in cases like this one. Unfortunately it is not very well documented. I hope this post will help in changing that.

7 comments:

  1. arguments.callee

    never heard of it, exactly what i needed

    thanks

    ReplyDelete
  2. Thanks so much, I am coding in Strict Mode so global variables were not the solution. This is exactly what I was looking for!
    Thanks, webmaster 2 webmaster!

    ReplyDelete
  3. replace

    this.removeEventListener('click', arguments.callee, false);

    by

    this.removeEventListener(e.type,arguments.callee,e.eventPhase);

    and it can cath any type of event no matter if is applied for the capture or bubbling phase.

    ReplyDelete
  4. Very nice! This is a very elegant solution. I love avoiding the nuisance of remembering a reference to the original callback.

    ReplyDelete
  5. Note that "arguments.callee" is not longer supported in strict mode (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode#Making_eval_and_arguments_simpler).

    ReplyDelete
  6. I recently stumbled upon a similar problem, when wanting to remove an event listener after it had been fired once. However, the solution with arguments.callee was not sufficient in my scenario, so I went with another option, to rebind the 'this' reference in the called function. For reference, here is how that is done: http://jsfiddle.net/roenbaeck/vBYu3/

    ReplyDelete