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