Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript: Creating Functions in a For Loop

Recently, I found myself needing to create an array of functions. The functions use values from an XML document, and I am running through the appropriate nodes with a for loop. However, upon doing this, I found that only the last node of the XML sheet (corresponding to the last run of the for loop) was ever used by all of the functions in the array.

The following is an example that showcases this:

var numArr = []; var funArr = []; for(var i = 0; i < 10; ++i){     numArr[numArr.length] = i;     funArr[funArr.length] = function(){  return i; }; }  window.alert("Num: " + numArr[5] + "\nFun: " + funArr[5]()); 

The output is Num: 5 and Fun: 10.

Upon research, I found a a segment of code that works, but I am struggling to understand precisely why it works. I reproduced it here using my example:

var funArr2 = []; for(var i = 0; i < 10; ++i)     funArr2[funArr2.length] = (function(i){ return function(){ return i;}})(i);  window.alert("Fun 2: " + funArr2[5]()); 

I know it has to do with scoping, but at first glance it does not seem like it would perform any differently from my naive approach. I am somewhat of a beginner in Javascript, so if I may ask, why is it that using this function-returning-a-function technique bypasses the scoping issue? Also, why is the (i) included on the end?

Thank you very much in advance.

like image 321
Ardeol Avatar asked Oct 30 '13 23:10

Ardeol


People also ask

Can you put a function inside a for loop JavaScript?

When we log a function call expression the output is the return value of the function. We logged the return value of a self-invoking (it called itself) anonymous function expression. This proves that we can run a function inside a loop.

What does () => mean in JavaScript?

It's a new feature that introduced in ES6 and is called arrow function. The left part denotes the input of a function and the right part the output of that function.

Can you have two for loops in a function JavaScript?

Can I run two for loops in one statement in javascript? Would I do it like this? TL;DR: Yes, in the initializer and loop (not the condition) sections, with commas.


2 Answers

The second method is a little clearer if you use a parameter name that does not mask the loop variable name:

funArr[funArr.length] = (function(val) { return function(){  return val; }})(i); 

The problem with your current code is that each function is a closure and they all reference the same variable i. When each function is run, it returns the value of i at the time the function is run (which will be one more than the limit value for the loop).

A clearer way would be to write a separate function that returns the closure that you want:

var numArr = []; var funArr = []; for(var i = 0; i < 10; ++i){     numArr[numArr.length] = i;     funArr[funArr.length] = getFun(i); }  function getFun(val) {     return function() { return val; }; } 

Note that this is doing basically the same thing as the first line of code in my answer: calling a function that returns a function and passing the value of i as a parameter. It's main advantage is clarity.

EDIT: Now that EcmaScript 6 is supported almost everywhere (sorry, IE users), you can get by with a simpler approach—use the let keyword instead of var for the loop variable:

var numArr = []; var funArr = []; for(let i = 0; i < 10; ++i){     numArr[numArr.length] = i;     funArr[funArr.length] = function(){  return i; }; } 

With that little change, each funArr element is a closure bound do a different i object on each loop iteration. For more info on let, see this Mozilla Hacks post from 2015. (If you're targeting environments that don't support let, stick with what I wrote earlier, or run this last through a transpiler before using.

like image 196
Ted Hopp Avatar answered Oct 05 '22 23:10

Ted Hopp


Let's investigate what the code does a little closer and assign imaginary function names:

(function outer(i) {      return function inner() {          return i;     }  })(i); 

Here, outer receives an argument i. JavaScript employs function scoping, meaning that each variable exists only within the function it is defined in. i here is defined in outer, and therefore exists in outer (and any scopes enclosed within).

inner contains a reference to the variable i. (Note that it does not redefine i as a parameter or with the var keyword!) JavaScript's scoping rules state that such a reference should be tied to the first enclosing scope, which here is the scope of outer. Therefore, i within inner refers to the same i that was within outer.

Finally, after defining the function outer, we immediately call it, passing it the value i (which is a separate variable, defined in the outermost scope). The value i is enclosed within outer, and its value cannot now be changed by any code within the outermost scope. Thus, when the outermost i is incremented in the for loop, the i within outer keeps the same value.

Remembering that we've actually created a number of anonymous functions, each with its own scope and argument values, it is hopefully clear how it is that each of these anonymous functions retains its own value for i.

Finally, for completeness, let's examine what happened with the original code:

for(var i = 0; i < 10; ++i){     numArr[numArr.length] = i;     funArr[funArr.length] = function(){  return i; }; } 

Here, we can see the anonymous function contains a reference to the outermost i. As that value changes, it will be reflected within the anonymous function, which does not retain its own copy of the value in any form. Thus, since i == 10 in the outermost scope at the time that we go and call all of these functions we've created, each function will return the value 10.

like image 22
Chris Hayes Avatar answered Oct 05 '22 23:10

Chris Hayes