2

I'm studying closures but I don't understand why the second is always 1 instead of 2, 3, 4, 5 respectively. What I understand is here that since each function (even iife) has its own scope. Each i variable is captured its own stack actually that's why it allows changes of values of i though had been var used.

In the same way, setTimeout should catch each different i variables having 1,2,3,4,5 (seconds) respectively. But, it doesn't seem such as seen in the image. Could you help?

  • Maybe it is helpful that i is not a free variable for setTimeout. (no idea it is of any relation)

Code

for (let i = 1; i <= 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, i * 1000)
  })(i)
}

a

Hax
  • 133
  • 9
  • what is your expected output of the `console.log`? – Amauri Jan 30 '21 at 20:38
  • What's shown in the image is what you seem to expect (and is correct). What's the question? What are you expecting to be different? Also, side note: Since you're using `let`, each loop iteration gets its own execution context and its own copy of the `i` in the `for` statement, so you don't need the IIFE at all. You only needed it before `let`, because `var` behaves differently. See http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – T.J. Crowder Jan 31 '21 at 13:35
  • *"I don't understand why the second is always 1"* The second what? I see only one code block in your question. – T.J. Crowder Jan 31 '21 at 13:36
  • @T.J.Crowder I expect that 1 second later 1, 2 secs later 2, 3 secs later 3, 4 secs later 4, 5 secs later 5. But 1,2,3,4,5 are printed 1 second elapsed each time as in the image. – Hax Jan 31 '21 at 20:29

4 Answers4

1

I think the use of let and an IIFE is throwing you off. Using an IIFE creates a new scope for the function parameters. A less known fact is that using let in a loop creates a new scope on every iteration.

e.g.

Using var and an IIFE.

// 'i' is declared here once
for (var i = 1; i <= 5; i++) {

  // 'i' is declared again and takes a new scope every time the function is executed
  (function (i) {
    setTimeout(function () {
      console.log(i) // references the nearest-scoped 'i'
    }, i * 1000)
  })(i)
}

// => 1, 2, 3, 4, 5

Using var and no IIFE

// 'i' is declared here once
for (var i = 1; i <= 5; i++) {
    setTimeout(function () {
      console.log(i) // references nearest-scoped 'i'
    }, i * 1000)
}

// => 6, 6, 6, 6, 6

Using let and no IIFE

// 'i' is given a new scope in every iteration
for (let i = 1; i <= 5; i++) {
    setTimeout(function () {
      console.log(i)
    }, i * 1000)
}

// => 1, 2, 3, 4, 5

Using let and an IIFE. This is exactly your example.

// 'i' is given a new scope in every iteration
for (let i = 1; i <= 5; i++) {

  // 'i' is declared again and takes a new scope every time the function is executed
  (function (i) {
    setTimeout(function () {
      console.log(i) // references the nearest-scoped 'i'
    }, i * 1000)
  })(i)
}

// => 1, 2, 3, 4, 5
Amauri
  • 540
  • 3
  • 15
0

I don't understand too how the engine works, albeit i gives results as expected. I wouldn't expect persistently 1 second elapsing.

    for (let i = 1; i <= 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, (console.log(i),i * 1000))
  })(i)
}
0

In a comment you've said:

I expect that 1 second later 1, 2 secs later 2, 3 secs later 3, 4 secs later 4, 5 secs later 5. But 1,2,3,4,5 are printed 1 second elapsed each time as in the image

That would be what would happen if you started the timer for 2 after the callback for timer 1 was called, but that's not what the code does. The code sets up the five timers all right away — a timer to show 1 after one second, a timer to show 2 after two seconds, a timer to show 3 after three seconds, etc. Since they all start at the same time, you see 1 after one second, 2 a second after that (two seconds after it was scheduled), 3 a second after that (three seconds after it was scheduled), etc.

To see that, let's show when we start each timer, and also how long it's been since the previous one fired. First, though, you don't need your IIFE, because you're using let in a for statement, so each loop iteration gets its own i variable. So here's your original code without the IIFE but adding the message when each timer starts and showing how long it was between when the timer started and when it fired; this will still just do them roughly one second apart instead of doing what you said you want:

// Still doesn't do what you want, but shows that the IIFE isn't needed
// and when each timer is set up
let previousFired = Date.now();
for (let i = 1; i <= 5; i++) {
    console.log(`setting timer to show ${i} in ${i * 1000}ms`);
    const started = Date.now();
    setTimeout(function () {
        console.log(`${i} - fired after ${Date.now() - started}ms (${Date.now() - previousFired}ms after last)`);
        previousFired = Date.now();
    }, i * 1000);
}

If you want to wait one second to see 1, then wait a further two seconds to see 2, etc., either don't start the next timer until the previous one fires, or schedule them later. The latter is simpler, so let's do that:

let total = 0;
let previousFired = Date.now();
for (let i = 1; i <= 5; i++) {
    total += i;
    console.log(`setting timer to show ${i} in ${total * 1000}ms`);
    const started = Date.now();
    setTimeout(function () {
        console.log(`${i} - fired after ${Date.now() - started}ms (${Date.now() - previousFired}ms after last)`);
        previousFired = Date.now();
    }, total * 1000);
}
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
-1

You can handle async loops awaiting a timedout Promise, then you can call a external function:

function f(i) {
  console.log(i)
}

const runLoop = async () => {
    for(let i = 1; i <= 5; i++){
        await new Promise(resolve => setTimeout(resolve, i * 1000))
        f(i * 1000);
    }
}

runLoop()
sonEtLumiere
  • 4,461
  • 3
  • 8
  • 35
  • Thanks for the view but I'm in the search of _internals_... – Hax Jan 30 '21 at 19:59
  • Check out this: https://stackoverflow.com/questions/11488014/asynchronous-process-inside-a-javascript-for-loop#:~:text=This%20is%20because%20the%20for,when%20those%20async%20operations%20finish. – sonEtLumiere Jan 30 '21 at 20:01