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>

2
  • 1
    Possible duplicate of How do JavaScript closures work? Commented Dec 12, 2017 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. Commented Dec 12, 2017 at 12:57

4 Answers 4

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..

Sign up to request clarification or add additional context in comments.

1 Comment

Ah now I see, thanks for the explanation! I think I did not expect these side effects as they differ from other functional languages.
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>

1 Comment

Thanks for the explanation and solution!
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>

2 Comments

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?
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
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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.