-3

Consider this classic JavaScript closure function. I understand how the closure is exhibited. I understand that the inner function closes in on the variable i, which is 3.

What I dont understand is why the array should contain the variable i, when all we are doing is pushing a function into an array where i has a value from the for loop.

function buildFunctions() {

  var arr = [];

  for (var i = 0; i < 3; i++) {
    arr.push(function() {
      console.log(i)
    })
  }
  return arr;
}

var fs = buildFunctions(); // [function(){console.log(1)}, ...and so on]
//not [function(){console.log(i)} ...and so on]

fs[0](); // outputs 3
fs[1](); // 3
fs[2](); // 3

otherwise, this would return the correct(imo) contents of the array:

function buildFunctions() {
   var arr = [];
   for (var i = 0; i < 3; i++) {
      arr.push(i)
   }
   return arr; // [0, 1, 2]
 }
AppyGG
  • 381
  • 1
  • 6
  • 12
Fatah
  • 2,184
  • 4
  • 18
  • 39
  • The variable `i` is *shared* by all of the functions your code creates. At the end of the loop, the value of `i` is 3, so that's what you get when you call the functions. – Pointy May 04 '18 at 14:18
  • 1
    Use `for (let i=0...`, it works, because it creates a local scope for `i`. – Jeremy Thille May 04 '18 at 14:19
  • I don't understand the question. `i` refers to the index of the array and is required to use the `for-loop`. In your first example, the array contains three functions, in your second example, it contains three numbers. You could omit the loop and just write `arr.push(0); arr.push(1); arr.push(2);` . – Shilly May 04 '18 at 14:20
  • I know this, @Pointy, the for loop is pushing a function that should be having a value, 0, 1 & then 2. Like that last function – Fatah May 04 '18 at 14:21
  • 1
    Possible duplicate of [JavaScript closure inside loops – simple practical example](https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Tan Duong May 04 '18 at 14:21
  • @Shilly why doesnt console.log(i) not pushed into the array with its value though? – Fatah May 04 '18 at 14:22
  • @fatahn — Because there is no reason for the value to be stored in the array. – Quentin May 04 '18 at 14:24
  • 1
    It logs a _reference_ to `i`, not its _value_ at the moment it's pushed. All three references are the same. – Jeremy Thille May 04 '18 at 14:24
  • 1
    Because you push a function, not the result of a function. What you say would only work if the code is like this: `arr.push(function(){return i;}());` Then you would create a function, immediately call it and add the returned value of i to the array instead of the function itsself. Console.log() does Not return anything, it only logs to the console. So it will never return its result to the array. It sounds a bit like you're mixing up closures, immediately invoked functions and return values. – Shilly May 04 '18 at 14:28

4 Answers4

3

arr.push(i) passes a primitive value to .push, the values 0, 1 and 2 respectively. The value becomes disassociated from i here; you're not pushing i, you're pushing 0, 1 and 2.

arr.push(function () { console.log(i) }) pushes a function, which internally references a variable. At the time that you call the function, the value of that variable happens to be 3. ( This is the critical sentence to understand.)

Note that there's no fundamental difference between pushing a function and pushing a number. Both are simply passed by value. Just in one case the value is a number and in the other case the value is a function. See Is JavaScript a pass-by-reference or pass-by-value language?.

deceze
  • 510,633
  • 85
  • 743
  • 889
  • I almost flagged this question as duplicate of the closures question but realized its different, thanks for that critical bit. – Fatah May 04 '18 at 17:08
2

I think the loop adds confusion for some reason. If you unroll that loop, it likely is more intuitive.

function buildFunctions() {

  var arr = [];
  var i = 0;

  arr.push(function() {
    console.log(i)
  })

  i++;

  arr.push(function() {
    console.log(i)
  })

  i++;

  arr.push(function() {
    console.log(i)
  })

  i++;
  
  return arr;
}

var fs = buildFunctions(); // [function(){console.log(1)}, ...and so on]
//not [function(){console.log(i)} ...and so on]

fs[0](); // outputs 3
fs[1](); // 3
fs[2](); // 3

So you can see that we're pushing three functions into the array, and in between, we're incrementing i. You can also see that all three functions are "looking at" the same i variable.

The variable is not read until the function is invoked, so because they're all "looking at" the same variable, they're naturally going to give the same result when finally invoked. And because the i was incremented three times before any of the invocations, the value returned will be 3.


As an exercise, change each function to add another i++ within. You'll see that no only are they all reading the same variable, but they all can mutate that same variable too.

  • Makes a lot of sense. Thanks. Is it correct to say that, before the function call, the contents of the array inside the function are: `[fn(){console.log(0)}, fn(){console.log(0)}, fn(){console.log(2)}]` while the scope context still exists? – Fatah May 04 '18 at 16:42
  • 1
    Not really. That would imply that the values of `i` are copied into the functions. Instead the functions only have the ability to reference `i`, so it would be more like `[fn(){console.log(i)}, fn(){console.log(i)}, fn(){console.log(i)}]` So when each function is invoked, it uses that `i` reference to read the *current* value of `i`. –  May 04 '18 at 16:45
  • 1
    ...the scope of the `buildFunctions` call will *continue to exist* as long as at least one of those functions that points to `i` continues to exist. –  May 04 '18 at 16:46
1

By the time you call the function the value of i becomes 3 which the function inside arr.push refer.

Block scope let will give you the expected result:

function buildFunctions() {

   var arr = [];

   for (let i = 0; i < 3; i++) { 
       arr.push(function() { 
          console.log(i)
       }) 
   }
   return arr;
}

var fs = buildFunctions(); // [function(){console.log(1)}, ...and so on]
                           //not [function(){console.log(i)} ...and so on]

fs[0](); // 0
fs[1](); // 1
fs[2](); // 2
Mamun
  • 66,969
  • 9
  • 47
  • 59
1

What I dont understand is why the array should contain the variable i

It doesn't.

The array contains three functions.

Each of those functions closes over the (same) variable i. Note: i not the value of i at the time.

When you call any of the functions (which is after the loop has finished), they read the value of that variable.

Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335