I'm having some issues with scoping in JavaScript when generating a function from within a loop.
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'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.
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.
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
.
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.
function*
generator declaration properly?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>
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With