0

Quite simply, I'd like to know why the call to arr0 seems to drag in the value of i instead of the one stored in the function at that position.

<script>
  var arr = [];

  for(var i = 0; i < 3; i++) {
    //Assign anonymous functions to the array in positions 0 to 2
    arr[i] = function() { console.log("function " + i); }
  }

  for(var i = 0; i < 3; i++) {
    //The output for these function calls is correct!
    arr[i]();
  }

  //Here I expected to output: function 0, but instead outputs: function 3 WTF!
  arr[0] ();

</script>

Here's the output:

function 0
function 1
function 2
function 3

For the last call, i.e: arr[ 0 ] (); I expected the output to be "function 0", but surprisingly IT'S NOT... Could someone please care to explain why?

Thanks in advance!

jlstr
  • 2,986
  • 6
  • 43
  • 60
  • 2
    possible duplicate of [Javascript infamous Loop problem?](http://stackoverflow.com/questions/1451009/javascript-infamous-loop-problem) – elclanrs Dec 13 '13 at 01:59
  • your `i` in main scope, so. when you looping second time you changing it, but all the functions you defined are same (you just place to array the reference to one function), so when you call `arr[0]()` you used `i` which defined in last loop (3) use function generator like in link above. try to use `for(var n = 0; n < 3; n++){arr[n]()}` to understand effect. – zb' Dec 13 '13 at 02:06
  • How would you guys implement it to do what I REALLY want it to do? – jlstr Dec 13 '13 at 02:08
  • 1
    `arr[i] = (function(i) { return function() {console.log("function " + i);}; })(i)` – zb' Dec 13 '13 at 02:11

3 Answers3

1

It's a common problem, after you've read the duplicate posted in the comments, here's a possible solution:

var arr = [0,1,2].map(function(i){
  return function(){
    console.log("function " + i);
  };
});

for(var i = 0; i < 3; i++) {
  arr[i]();
}

arr[0]();

By creating an isolate scope with map we avoid the problem altogether. Using underscore's _.range you could replace your loop patterns with:

_.range(0,10).map(function(index){
  ...
})
elclanrs
  • 92,861
  • 21
  • 134
  • 171
  • Thank you for your response, sir. One Question: Where does the i parameter of the map() callback come from? – jlstr Dec 13 '13 at 02:21
  • `Array.prototype.map` expects the first argument to be the value and second one the index, `.map(function(value, index){})` but in this case the value **is** the index. – elclanrs Dec 13 '13 at 02:22
1

The question linked to in one of the comments will give you the general answer.

But I'll also specifically address what you're seeing here, because it might still be slightly confusing even after understanding the answers to the other question.

The main thing to realize here is that there is only one i variable. Even though it looks like you're declaring it twice with the var keyword, the second "declaration" is essentially ignored and treated like any ordinary assignment. You see, the for keyword does not introduce a new scope in JavaScript. So these two snippets are equivalent:

for (var i = 0; i < 3; i++) {}

And:

var i;
for (i = 0; i < 3; i++) {}

Once you realize that, and you get that the functions you create in the first loop all close over the same i, then you can understand why the second loop appears to be "correct" by your intuition: at the start of the loop you set i to 0, and then after each call you increment it. So even though all of them close over the same i, you're changing its value between calls!

And of course, for that last call, the value of i is still 3 since that's what it was at the end of the second loop and you didn't change it from that.

Community
  • 1
  • 1
Dan Tao
  • 125,917
  • 54
  • 300
  • 447
1

Well this is a mixed bunch...

You are using the same i variable (despite "re-defining" it in the second loop, it's still the same i) that is placed in the global scope

As a result, in the second loop, each iteration alters the value of that global i, which results in the

function 0
function 1
function 2

function 3

output.

It was, by the way, absolutely not the expected result, if you used k in the second loop:

<script>
  var arr = [];

  for(var i = 0; i < 3; i++) {
    //Assign anonymous functions to the array in positions 0 to 2
    arr[i] = function() { console.log("function " + i); }
  }

  for(var k = 0; k < 3; k++) {
    //The output for these function calls is correct!
    arr[k]();
  }

  //Here I expected to output: function 0, but instead outputs: function 3 WTF!
  arr[0] ();

</script>

That would produce:

function 3 
function 3 
function 3

function 3

and that is the infamous loop problem referred in the link above (in comments).

The reason is that functions, defined in your first loop (which, BTW, you should try to avoid in general case), "close" on the variables that are in the same scope as their definition - i in this case. That means that whenever value of i is changed later on, it will be reflected in that function.

The last example shows it in action - i is changed by the first for loop, so when the loop is finished - it's value is 3. All functions you defined now have the same value of i - 3.

To make the output like this:

function 0
function 1 
function 2    
function 0

you can do this (not that it's that great, structure wise):

var arr = [];

for(var i = 0; i < 3; i++) {
    //Assign anonymous functions to the array in positions 0 to 2
    arr[i] = (function(index){ return function() { console.log("function " + index); };}(i));
}

for(var k = 0; k < 3; k++) {
    //The output for these function calls is correct!
    arr[k]();
}

//Here I expected to output: function 0, but instead outputs: function 3 WTF!
arr[0] ();

That produces the ddesire result.

Here, you define an anonymous function:

(function(index){ ... }(i))

that is immediately invoked with i as a parameter. That parameter is referred to as index in the function body (not that it's important, even if you still called it i - it would work, since the "inner" i would shadow the "outer" one).

That function returns a function that has a different closure - on index, which is not important since index is not avialable after the immediately invoked function exists.

Another way would be using some sort of iterator - map, where supported, would do OK.

ZenMaster
  • 12,363
  • 5
  • 36
  • 59
  • Excellent answer. so basically, JavaScript tends to "close" by default, and to to prevent this we need just another internal scope layer (function(index) in this case) ? – jlstr Dec 13 '13 at 03:09
  • That is the gist of it. – ZenMaster Dec 13 '13 at 03:26
  • Well, learning JavaScript is truly giving me headaches, I think I know it, and then stuff like this pops up! Thank you for your fantastic answer, sir. – jlstr Dec 13 '13 at 03:33
  • Don't worry. I've been doing it for almost a decade and I still have these moments. – ZenMaster Dec 13 '13 at 03:34