Search code examples
javascriptclosureseventhandler

Closure in event handler


var element = document.getElementById('test-button');

//event handler
element.onclick = (function outer() {

  var counter = 0;

  return function inner() {

    counter++;
    alert('Number of clicks: ' + counter);
  };
})();
<button id="test-button">Test</button>

I don't understand why the variable counter won't reset to 0 every time the button is clicked, or maybe when it's clicked, only the function inner() get executed, then why the variable counter get set to 0 when first click occurs. The code is from this link, after carefully read their explanation, I'm still confused.


Solution

  • why the variable counter get set to 0 when first click occurs.

    That is not the moment it gets set to 0. This happens when the assignment to .onclick happens. At that moment outer is defined and executed. So that is the moment counter is set to 0. No click is handled yet.

    only the function inner() get executed

    Indeed. outer is only executed once, when the main script is executed on page load, and then never again. inner is the function that gets assigned to onclick and thus has the role of event handler. inner has access to the counter that is defined in outer, but outer is never executed again.

    outer is a so-called immediately executing function.

    As a comparison: you could achieve something similar with this code:

    var counter = 0;
    element.onclick = function inner() {
        counter++;
        alert('Number of clicks: ' + counter);
    };
    

    ...but the downside now is that counter is a global variable, that could be altered by some other code. To make sure counter is only available to the click handler, a closure is created for it, which you could also do like this:

    function outer() {
        var counter = 0;
        element.onclick = function inner() {
            counter++;
            alert('Number of clicks: ' + counter);
        };
    }
    outer();
    

    The downside here is that outer has a so-called side-effect: it assigns to element.onclick. This is the reason to choose to return inner and leave it to the caller of outer to do something with it, e.g. assign it to .onclick.

    Then we get something like this:

    function outer() {
        var counter = 0;
    
        return function inner() {
            counter++;
            alert('Number of clicks: ' + counter);
        };
    }
    
    element.onclick = outer();
    

    This is essentially the same as your code: the function is first defined, and then immediately executed. The function that gets assigned to onclick is the one that is returned by outer, i.e. inner.

    Yet another way to achieve this encapsulation of counter:

    function inner() {
        this.counter++;
        alert('Number of clicks: ' + this.counter);
    }
    
    element.onclick = inner.bind({ counter: 0 });
    

    Here the we provide an unnamed object that is always passed as this-argument to inner whenever it is called. As we don't retain a reference to that object outside of this construct, we achieve the same principle: the counter's state is maintained, but is not accessible outside the handler.