0

When creating a set of buttons I came across a strange behaviour. When writing it first, I expected every button to display its own number when clicked, but apparently this is not the case. But strangely I still get two different behaviours depending on how I define the string that should be displayed. So my questions are:

  1. Why do the two examples produce different outcomes, ans why are they different from what is displayed when we assign something using innerHTML?
  2. What do I have to change for getting the outcome I originally wanted? (Each button click displays own number.)

function test(){

  var foo = document.getElementById("foo")
  var bar = document.getElementById("bar")
  
  for(var k=0; k < 2; k++){
  
    // 1st example
    var a = document.createElement("button")
    a.innerHTML = "A button "+k
    var str = "click " + k
    a.onclick = function(){alert(str)}
    foo.appendChild(a)
    
    // 2nd example
    var b = document.createElement("button")
    b.innerHTML = "B button "+k
    b.onclick = function(){alert("click " + k)}
    bar.appendChild(b)
    
  }
}

test()
<div id="foo">

</div>
<div id="bar">

</div>
flawr
  • 10,814
  • 3
  • 41
  • 71
  • 1
    Possible duplicate of [How do JavaScript closures work?](https://stackoverflow.com/questions/111102/how-do-javascript-closures-work) – Martin Schneider Dec 12 '17 at 12:47
  • @MartinSchneider I see that your linked question does contain the answer to my question, but as the answers there comprise a *lot* more than what is asked here, so it would be helpful to point what part to read in order to understand this behaviour. – flawr Dec 12 '17 at 12:57

4 Answers4

2

This is a common problem. What happens is that there's only one var k (and also one str) for all the buttons, and the results reflect that. In the A case, the last time the str updates is when k==1, and that is displayed. In the B case, k becomes 2 when the loop stops, and so that is displayed.

One way to fix that would be the following, which works because each time you call a function, a new set of variables is created.

function makeOnClick(k) {
    var msg = "click " + k;
    return function() {
        alert(msg);
    };
}

function test() {
    var foo = document.getElementById("foo")
  
    for (var k = 0; k < 2; k++) {
        var a = document.createElement("button");
        a.innerHTML = "A button " + k;
        a.onclick = makeOnClick(k);
        foo.appendChild(a)
    }
}

test();
<div id="foo">

</div>

Alternatively, you could pull the entire creation of a button into a separate function, and that would also work:

function makeButton(k) {
    var button = document.createElement("button");
    button.innerHTML = "A button " + k;
    button.onclick = function() {
        alert("Click " + k);
    };
    return button;
}

function test() {
    var foo = document.getElementById("foo")
  
    for (var k = 0; k < 2; k++)
        foo.appendChild(makeButton(k));
}

test();
<div id="foo">

</div>

Bottom line - you need at least one function call per button somewhere, so the button gets its own set of variables..

xs0
  • 2,990
  • 17
  • 25
  • Ah now I see, thanks for the explanation! I think I did not expect these side effects as they differ from other functional languages. – flawr Dec 12 '17 at 12:54
1

You need to do some reading on JavaScript closures. Basically here your k variable is defined outside of the click callbacks. So when you click the buttons, not matter which one, k will always be equal to it's last value in the for loop (k = 1).

One way to get the result you want is to encapsulate your variable in a function, like so:

function test(){

  var foo = document.getElementById("foo")
  var bar = document.getElementById("bar")
  
  for(var k=0; k < 2; k++){
  
    // 1st example
    var a = document.createElement("button")
    a.innerHTML = "A button "+k
    a.onclick = (function(num){
      return function () {
        alert("click " + num);
      }
    })(k);
    foo.appendChild(a)
    
    // 2nd example
    var b = document.createElement("button")
    b.innerHTML = "B button "+k
    b.onclick = (function(num){
      return function () {
        alert("click " + num);
      }
    })(k);
    bar.appendChild(b);
    
  }
}

test()
<div id="foo">

</div>
<div id="bar">

</div>
klugjo
  • 19,422
  • 8
  • 57
  • 75
1

Javascript has a feature called hoisting. No matter where you declare a var in a function, the declaration is hoisted or pulled to the beginning of the function.

Additionally vars are not block-scoped. So your vars in your loop are pulled to the beginning of your function outside of your loop and you have only one version of it, not serveral Version in each iteration.

And then this single variable is referenced by a closure inside the function that is attached to the clicklistener of your button. Since this clicklistener is called after the loop has finished, the value of the var will be the last one from the last Iteration.

If you have the chance to make some architectural decisions you could consider using Es 2015 where the new Keyword let has been introduced. let is block-scoped and also prevents declaring the same variable more then one time. So this behaves more like one would expect it especially when you are used to other languages. Infact it is good practice not to use var at all in Es 2015.

treeno
  • 2,510
  • 1
  • 20
  • 36
1

Another solution using let instead of var

More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

function test(){

  var foo = document.getElementById("foo")
  var bar = document.getElementById("bar")
  
  for(let k=0; k < 2; k++){
  
    // 1st example
    let a = document.createElement("button")
    a.innerHTML = "A button "+k
    let str = "click " + k
    a.onclick = function(){alert(str)}
    foo.appendChild(a)
    
    // 2nd example
    let b = document.createElement("button")
    b.innerHTML = "B button "+k
    b.onclick = function(){alert("click " + k)}
    bar.appendChild(b)
    
  }
}

test()
<div id="foo">

</div>
<div id="bar">

</div>
jesusnoseq
  • 341
  • 4
  • 9
  • Oh I was not aware of that, this is very useful to know! So is it correct that if we use `var a = ...` in the loop, a new variable is created only in the first iteration, while when using `let a = ...` we are creating a new variable in each iteration? – flawr Dec 12 '17 at 13:12
  • Both variables are declared only once in the `for` loop. The difference between `var` and `let` is the scope. `var` is scoped to the nearest function block and `let` is scoped to the nearest enclosing block. Also, if you use strict mode you can't redeclare a let variable. [link](https://stackoverflow.com/questions/762011/whats-the-difference-between-using-let-and-var-to-declare-a-variable) – jesusnoseq Dec 12 '17 at 15:55