JavaScript gotcha: scoping and loops

May 22nd, 2012 Chris Winters, Consultant  (email the author)

(or: one way to wield immediately-invoked function expressions)

JavaScript has some interesting scoping rules. One of them is
that variables are scoped to functions rather than blocks. This
sounds simple but it can have some pretty serious consequences.

Say we’re transforming an array of arbitrary objects into
a simple JavaScript object. In this case, each item in the array
maps to a key/value pair, where the key is its 1-based array index
and the value is a function which just returns the original array
value. Here’s a first attempt:

function createMapped_1( items ) {
    var mapped = {};
    for ( var i = 1; i <= items.length; i++ ) {
        mapped[i] = function() { return items[i-1] };
        show_kv( "In ", i, mapped[i]() );
    }
    print( "Last index is: " + i );
    return mapped;
}

function showMapped( map ) {
    for ( var key in map ) {
        show_kv( "Out", key, map[ key ]() );
    }
}

function show_kv( type,  key, result ) {
    print( type + " [Key: " + key + "] " +
           "[Result: " + result + "]" );
}

var birds = [ 
  'owl', 'robin', 'eagle', 'sparrow', 'falcon' 
];
showMapped( createMapped_1( birds ) );

Executing this you’d expect both the ‘In’ and ‘Out’ sets of output to
match. Instead, you get:

In  [Key: 1] [Result: owl]
In  [Key: 2] [Result: robin]
In  [Key: 3] [Result: eagle]
In  [Key: 4] [Result: sparrow]
In  [Key: 5] [Result: falcon]
Last index is: 6
Out [Key: 1] [Result: undefined]
Out [Key: 2] [Result: undefined]
Out [Key: 3] [Result: undefined]
Out [Key: 4] [Result: undefined]
Out [Key: 5] [Result: undefined]

Not only is that wrong, but in many languages you’d get a
compilation error because the variable i wouldn’t be visible
outside the scope of the surrounding block (the for loop). For
example, here’s a simple Java class to demonstrate:

public class OutsideBlock {
    public static void main( String... args ) {
        for ( int i = 0; i < 5; i++ ) {
            System.out.println( "In loop: " + i );
        }
        System.out.println( "Out of loop: " + i );
    }
}

Compiling this gives:

$ javac OutsideBlock.java 
OutsideBlock.java:6: cannot find symbol
symbol  : variable i
location: class OutsideBlock
        System.out.println( "Out of loop: " + i );
                                              ^
1 error

And a similar Perl program in strict mode…

use strict;

print( "In loop: $i\n" ) for my $i ( 0..4 );
print( "Out of loop: $i\n" );

…gives a similar complaint:

$ perl outside_block.pl 
Global symbol "$i" requires explicit package name at outside_block.pl line 5.
syntax error at outside_block.pl line 5, near "$i ( "
Execution of outside_block.pl aborted due to compilation errors.

Instead in JavaScript we get the last value of i that failed
the loop’s conditional test. That this variable is even
accessible is a glimpse at the function vs. block scope that
winds up biting us.

But back to our basic question: why does the closure that gets
executed within createMapped_1() not return the same value
as the same closure that gets executed outside
createMapped_1()?

What if we rewrote the function to assign the indexed value
from items outside the closure? Such as this:

function createMapped_2( items ) {
    var mapped = {};
    for ( var i = 1; i <= items.length; i++ ) {
        var value = items[i-1];
        mapped[i] = function() { return value };
        show_kv( "In ", i, mapped[i]() );
    }
    print( "Last index is: " + i );
    return mapped;
}

Running it we get:

In  [Key: 1] [Result: owl]
In  [Key: 2] [Result: robin]
In  [Key: 3] [Result: eagle]
In  [Key: 4] [Result: sparrow]
In  [Key: 5] [Result: falcon]
Last index is: 6
Out [Key: 1] [Result: falcon]
Out [Key: 2] [Result: falcon]
Out [Key: 3] [Result: falcon]
Out [Key: 4] [Result: falcon]
Out [Key: 5] [Result: falcon]

Different, but still wrong. The cause in both our initial attempt
and this one is the same, just with a different mask. The output
gives us a hint as to the cause — ‘falcon’ is the last element
of the array — but let’s make it more explicit and rephrase the
function to reflect how JavaScript scoping rules work:

function createMapped_3( items ) {
    var mapped = {};
    var i;
    var value;
    for ( i = 1; i <= items.length; i++ ) {
        value = items[i-1];
        mapped[i] = function() { return value };
        show_kv( "In ", i, mapped[i]() );
    }
    print( "Last index is: " + i );
    return mapped;
}

From that it’s apparent that both the variables i and value
aren’t scoped to the loop, but instead to the function and overwritten for
every iteration through the loop. So the closure just reflects
the most recent value assigned, which is why it returns the last
member of the array.

This also explains why in our first attempt we just got
undefined: the value of i outside the loop is 6, and
there’s no item at array index 6 which is why the closure returns
undefined.

OK, fine: how do we fix it? This is where the
immediately-invoked function expression comes in. We’re going
to create a function that returns a function, and then
immediately execute the first. So what happens if we wrap our
first attempt’s loop code with an IIFE? Here’s what it looks
like:

function createMapped_4( items ) {
    var mapped = {};
    for ( var i = 1; i <= items.length; i++ ) {
        (function() {
            mapped[i] = (function() {
              return function() { return items[i-1] } 
            })();
            show_kv( "In ", i, mapped[i]() );
        })();
    }
    print( "Last index is: " + i );
    return mapped;
}

And the output:

In  [Key: 1] [Result: owl]
In  [Key: 2] [Result: robin]
In  [Key: 3] [Result: eagle]
In  [Key: 4] [Result: sparrow]
In  [Key: 5] [Result: falcon]
Last index is: 6
Out [Key: 1] [Result: undefined]
Out [Key: 2] [Result: undefined]
Out [Key: 3] [Result: undefined]
Out [Key: 4] [Result: undefined]
Out [Key: 5] [Result: undefined]

Ouch, back to square one! And it’s for the same reason — the
value of i is bound to the outer function, so we’re still just
referencing that same value after the loop is finished. Let’s
rewrite it to use an assignment (like our second attempt, above)
but inside our IIFE instead of outside:

function createMapped_5( items ) {
    var mapped = {};
    for ( var i = 1; i <= items.length; i++ ) {
        (function() {
            var value = items[i-1];
            mapped[i] = function() { return value };
            show_kv( "In ", i, mapped[i]() );
        })();
    }
    print( "Last index is: " + i );
    return mapped;
}

The output:

In  [Key: 1] [Result: owl]
In  [Key: 2] [Result: robin]
In  [Key: 3] [Result: eagle]
In  [Key: 4] [Result: sparrow]
In  [Key: 5] [Result: falcon]
Last index is: 6
Out [Key: 1] [Result: owl]
Out [Key: 2] [Result: robin]
Out [Key: 3] [Result: eagle]
Out [Key: 4] [Result: sparrow]
Out [Key: 5] [Result: falcon]

Excellent! Our returned closure no longer references a variable
that’s defined outside the loop, so it resolves properly.

You may see a variant of this mechanism that passes in the
variables for the closure to reference, like:

function createMapped_6( items ) {
    var mapped = {};
    for ( var i = 1; i <= items.length; i++ ) {
        (function( map, key, value ) {
            map[key] = function() { return value };
            show_kv( "In ", i, mapped[i]() );
        })( mapped, i, items[i-1] );
    }
    print( "Last index is: " + i );
    return mapped;
}

This may feel cleaner because you’re explicitly localizing the
variables used by the closure. But you are introducing multiple
names for the same values which can be a problem for the humans
comprehending your code. And if your closure references many
variables it can get unwieldly, though that may point out the
need for other work to do — refactoring by consolidating
multiple parameters into an object, or breaking the closure into
multiple functions, or something else.

Additional reading:

Be Sociable, Share!

Entry Filed under: Agile and Development

1 Comment Add your own

Leave a Comment

Required

Required, hidden


four + = 7

Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Trackback this post  |  Subscribe to the comments via RSS Feed

© 2010-2014 Summa All Rights Reserved