javascriptclosureslexical-scope

Why doesn't assigning variables inside a function copy instead of referencing it


I have this code:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function () {
      console.log(i);
      // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.

This happens because i is always a reference to the outer i inside the makeArmy() function.

If we adjust the code to be:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    // copy the varible i into the while lexical scope
    let j = i
    let shooter = function () {
      console.log(j);
      // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();


army[0](); // 0
army[1](); // 1
army[2](); // 2

The above code works as expected because instead of referencing the variable i inside shooter() we are copying it into j, thus making it available to the shooter function in its own scope.

Now, my question is, why doesn't copying the i variable inside the function itself work? even though I'm effectively still copying the variable? What am I missing here?

Code:

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function () {
      let j = i;

      console.log(j);
      // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.

Solution

  • Because you are copying at a time at which i is already 10.

    In fact, with let j = i in the inner function, there will be no difference to using i directly, because that assignment also executes only when you invoke that inner function, and as said, at this point i will already be 10, since the invocation happens long after the loop completed, in e.g. army[0]().

    As you noted, it works if you move the let j = i outside of the inner function (but still inside the loop). This way, the assigment to the local variable(s) j happens on each iteration immediately, and the inner function later refers to those 10 different j's with their right values that were previously set at that point in time when i had the respective value that you wanted.


    By the way, this problem won't exist if you'd use a for loop instead because for with let has some "magic" that creates a semi-separated scope for each iteration (the exact way this works is a bit more complicated).

      for (let i = 0; i < 10; i++) {
        let shooter = function () {
          console.log(i);
        };
        shooters.push(shooter);
      }