Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variable in generated JavaScript function doesn't behave as expected [duplicate]

I'm having some issues with scoping in JavaScript when generating a function from within a loop.

What I Want:

The way I want this to work is a for loop that for each iteration, generates a function named doStuff + i. For example, the first iteration will generate doStuff1() the second will generate doStuff2() and so on and so forth. The functions themselves (for the sake of example) just need to print out i—that is, doStuff1() prints 1, doStuff2() prints 2, etc.

What I Get:

What's actually happening, is i doesn't 'stick' in the function. It becomes part of the global scope or something so it's 10 for every function. You can see this if you click the first button in the snippet below.

What I've Tried:

Using a Generator Function

In example two, I've tried using the function* notation to create a proper function generator. I'm pretty sure I implemented it before, but I've never used that notation before so I could be way off. Please let me know if that's the case.

The result of this is the same as example 2.

Using a String Instead of an Integer

For example three, I decided to try using a string instead of an integer and it works! For every iteration, a is appended to a string so when I run the generated functions in order, I get a nice little pyramid of the letter a.

Declaring and Assigning Variables in a Different Place

Since I had to define the stringOut variable in a different scope in example 3, I decided to try the same with numbers for example 4, and it worked again! This doesn't make much sense to me since being in a higher scope seems like it would be more likely to suffer from the same problems as example 1 & 2.

What I Want to Know:

  • How come examples 3 & 4 work, while 1 & 2 don't? Edit: My code was broken. None of it works.
  • Did I use the function* generator declaration properly?
  • What is the best (most simple, concise, and readable) way to do this? I know making an array of functions would probably be best, but in this situation that isn't possible.

Example Code:

function test1() {
  document.getElementById("output").innerHTML = "#1 Output:";
  var myFunctions = [];

  for (var i = 0; i < 10; i++) {
    myFunctions[i] = function() {
      document.getElementById("output").innerHTML += "<br>" + i;
    }
  }

  for (var j = 0; j < 10; j++) {
    myFunctions[j]();
  }
}

function test2() {
  document.getElementById("output").innerHTML = "#2 Output:";
  window.test2Funcs = {};
  function* Generator() {
    var functionName = "doStuff";
    var number = 0;
    while (number < 10) {
    number++;
    yield {
        myFunction: function() {
        document.getElementById("output").innerHTML += "<br>" + number;
        },
        name: functionName + (number)
      }
    }
  }
  
  var generator = new Generator();
  for (var k = 0; k < 10; k++) {
    var out = generator.next().value;
    window.test2Funcs[out.name] = out.myFunction;
  }
	
  for (var l = 1; l < 11; l++) {
    func = "doStuff" + l;
    test2Funcs[func]();
  }
}

function test3() {
  document.getElementById("output").innerHTML = "#3 Output:";
  var myFunctions = [];
  var stringOut = "";
  for (var i = 0; i < 10; i++) {
    stringOut += "a"; // Edit. Moved from within function below.
    myFunctions[i] = function() {
      document.getElementById("output").innerHTML += "<br>" + stringOut;
    }
  }

  for (var j = 0; j < 10; j++) {
    myFunctions[j]();
  }
}

function test4() {
  document.getElementById("output").innerHTML = "#4 Output:";
  var myFunctions = [];
  var numOut = 0; // Edit. Used to be var numOut = "";
  for (var i = 0; i < 10; i++) {
    numOut++; // Edit. Moved from within function below.
    myFunctions[i] = function() {
      document.getElementById("output").innerHTML += "<br>" + numOut;
    }
  }

  for (var j = 0; j < 10; j++) {
    myFunctions[j]();
  }
}

document.getElementById("test1").addEventListener("click", test1);
document.getElementById("test2").addEventListener("click", test2);
document.getElementById("test3").addEventListener("click", test3);
document.getElementById("test4").addEventListener("click", test4);
<button id="test1">1st Attempt</button>
<button id="test2">2nd Attempt</button>
<button id="test3">3rd Attempt</button>
<button id="test4">4th Attempt</button>
<div id="output">
Output:
</div>
like image 476
3ocene Avatar asked Dec 28 '15 17:12

3ocene


1 Answers

The problem is that you are not creating multiple functions in a loop, they are all the same instance of the function, sharing the same closure (which includes i). By the time the functions are called, i is the value it had when it exited the loop.

var funcs = [];
for (var i=0; i < 5; i++) {
   funcs.push(function() {
       console.log(i);
   });
}
// Here, i is 5
funcs[0](); // 5
funcs[1](); // 5
funcs[4](); // 5

How can you fix it? By adding an extra closure, I call that technique "freezing a closure"

function createHandler(val) {
  return function() {
    console.log(val);
  }
}
var funcs = [];
for (var i = 0; i < 5; i++) {
  // By calling a different function, a copy of i is passed to that function
  funcs.push(createHandler(i));
}
// Here, i is 5
funcs[0](); // 0
funcs[1](); // 1
funcs[4](); // 4

You could also use Function.bind

var funcs = [];
for (var i = 0; i < 5; i++) {
  // By calling bind, a new function is created, with a new closure
  funcs.push(
    (function(val) {
      console.log(val);
    }).bind(null, i)
  );
}
// Here, i is 5
funcs[0](); // 0
funcs[1](); // 1
funcs[4](); // 4
like image 117
Juan Mendes Avatar answered Sep 24 '22 00:09

Juan Mendes