1

I have the following code. Here I am trying to add click event handler to buttons on a page. The following code does not work and shows output as "[Object MoustEvent]".I know how to fix it (using another closure and wrap onclick handler around it), but I want to understand why I am getting this output. Can someone please explain?

document.addEventListener('DOMContentLoaded', function() {
  var prizes = ['A Unicorn!', 'A Hug!', 'Fresh Laundry!'];
  for (var btnNum = 0; btnNum < prizes.length; btnNum++) {
  
    // for each of our buttons, when the user clicks it...
    document.getElementById('btn-' + btnNum).onclick = function(btnNum) {
      var btnNum1 = btnNum;
      alert(btnNum1);
    };
  }
});
<button id="btn-0">Button 1!</button>
<button id="btn-1">Button 2!</button>
<button id="btn-2">Button 3!</button>
Karan
  • 12,059
  • 3
  • 24
  • 40
Shibasis Sengupta
  • 629
  • 1
  • 6
  • 21

2 Answers2

0

The code should be

document.getElementById('btn-' + btnNum).onclick = (function(btnNum){
    return function(){
        var btnNum1=btnNum;
        alert(btnNum1);
    }
})(btnNum);

The reason for the apparently strange pattern

(function(x){ return function(){ ... } })(x);

is because in (old) Javascript local variables have function scope, not block scope; the only way to create a new variable to capture is to use a (possibly nested) function.

More recent Javascript has let that allows creation of local variables without having to use this function + (return function) pattern.

Using let code like

for (let btnNum = 0; btnNum < prizes.length; btnNum++) {
    document.getElementById('btn-' + btnNum).onclick = function() {
        var btnNum1=btnNum;
        alert(btnNum1);
    };
}

will work as you expect.

6502
  • 112,025
  • 15
  • 165
  • 265
  • you only have half of the explanation here – Touffy Mar 13 '17 at 07:14
  • 1
    @Touffy: the parameter in the original code is IMO part of a function-return-function pattern being not copied correctly (because not being understood). Apparently you don't understand the Javascript `var` binding rules either: your answer is not correct because all the closures will capture the same variable and all the elements when clicked will display the same message (a closure captures a **variable**, not a **value**). – 6502 Mar 13 '17 at 07:56
  • That is correct. As I implied, you did describe part of the problem. Just not the part about the argument in the onclick callback, which you fixed in your code but didn't explain. And your comment above adds some useful explanation about why just creating a closure isn't enough in OP's case, which you should add to your answer. Just trying to improve things here… – Touffy Mar 13 '17 at 15:39
  • @6502 Awesome explanation. Thanks much for it. Why do you think as asked in the original question, I was receiving [Object MoustEvent] as output? – Shibasis Sengupta Mar 14 '17 at 00:53
  • @ShibasisSengupta it's a consequence of that argument thing I mentioned. I'll update my answer. – Touffy Mar 14 '17 at 07:15
  • @ShibasisSengupta: mouse events get passed an `Event` object, you should not declare them as receiving the target node. You probably saw code accepting the target node in an example that was using the function-in-function wrapper trick to solve problem of closures capturing variables, not values. Declaring an event handler as accepting the target dom node is nonsense, handlers are passed events so you either declare them as `function(){...}` ignoring the argument if you don't care about event details or as `function(event){...}` if for example you need mouse coordinates. – 6502 Mar 14 '17 at 09:54
0

Remove btnNum from the argument list of your function and the closure will work as intended (in .onclick = function(btnNum) {…) with the caveat 6502 mentionned (it's the same variable shared in each closure so it will be the same value).

The point of a closure is that you use a local variable from an outer scope in a function defined in that scope. When you do:

whatever.onclick = function(btnNum) {

    var btnNum1=btnNum;

    alert(btnNum1);
};

you are declaring btnNum as an argument of the function, which means it's a local variable and you cannot access the btnNum variable from outside the onclick function (its own btnNum is "shadowing" the one in the closure).

Since onclick is an even handler, the first argument passed to it is an event, and you're declaring that this argument should be called btnNum. So there you are. On the other hand, with this:

whatever.onclick = function(event) {

    var btnNum1=btnNum;

    alert(btnNum1);
};

the identifier "btnNum" cannot be resolved in the scope of the onclick function, so the interpreter goes up the scope chain and finds the "btnNum" variable in the scope of the outer function. The argument which I renamed to event would still be an Event object, if you cared to use it in your function.

Touffy
  • 6,309
  • 22
  • 28