4

From You Don't Know JS:

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

gives

6
6
6
6
6

but using an IIFE like so

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

gives

1
2
3
4
5

My question: why doesn't

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

or

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

work like the IIFE example? It seems to me they both have a function declaration with a new variable j, wouldn't that create a new lexical scope with a specific setting for i?

user1592772
  • 193
  • 1
  • 8

3 Answers3

5

The important part of the IIFE is that it runs right away; before i changes, it reads its value and puts it in a new variable. The function reading i in your other examples – function timer() – does not run right away, and the value it puts in its new variable is the value of i after it’s already changed.

Also, in ES6, you can just let i = … instead of var i = … and it’ll work fine without the IIFE or j:

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

because let has block scope instead of function scope and variables declared in the initialization part of for loops count as being half-inside the for’s block.

Ry-
  • 218,210
  • 55
  • 464
  • 476
  • I think introducing `let` at this point of time will makes him skip away from understanding the fundamental concept. :p – Isaac May 31 '18 at 02:06
  • 3
    @Isaac: If you never use `var`, skipping it’s probably fine =P – Ry- May 31 '18 at 02:07
  • seems to me like `let` is definitely the way to go, but `var` is good for understanding certain details – user1592772 May 31 '18 at 02:49
1

i, being declared with var, is hoisted. Variables don't automatically get their scopes bound to an inner function; unless the inner function explicitly has var i or a paramter of i (thus defining a new i bound to the scope of the inner function), i will continue to refer to the hoisted i in the outer scope.

For example, you could do what you were thinking of like this, if you wanted:

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

(The third argument to setTimeout is what the function, the second argument, will be called with)

This means that timer will be called with i as it is during iteration, and the function will use a new i, bound to the scope of the function, initialized via the parameter.

It's a pretty bad idea, though - better to use const and let, which have block scope rather than function scope, and better not to shadow outer variables.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    I don’t think this explanation really works, unless the OP thought named functions would work differently than anonymous ones. – Ry- May 31 '18 at 02:08
0

This sort of IIFE

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

is often written like

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

so, you can see that "captured" value is i in this case

You can do the same without IIFE

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

of course, this is equivalent of

function timer(j) {
    setTimeout(function() {
        console.log(j);
    }, j * 1000 );
}

for (var i=1; i<=5; i++) {
    timer(i);
}

if using ES2015+, you can use let

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

Now, if you use a transpiler because you need to support ES5 (or whatever internet exploder supports) you'll see that the transpiled version is

var _loop = function _loop(i) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
};

for (var i = 1; i <= 5; i++) {
    _loop(i);
}

Which looks incredibly like the previous version of the code

Jaromanda X
  • 53,868
  • 5
  • 73
  • 87