0
function endGame(num) {
    var total = Object.keys(abvMap).length;

    $("#result").empty();

    if(num == total) {
        // you won
        $("#result").append("<h4>YOU WON!</h4><p>You got all " + total + " states!</p>");
    } else {
        console.log(states);
        // didn't get all 51 states
        $("#result").append("<h4>ALMOST!</h4><p>You got " + num + " out of " + total +" states!</p><h3>Missing states:</h3><ul id=\"missing-states-list\"></ul>");
        for(var key in abvMap) {
            if(($.inArray(key, states)) == -1) {
                console.log(key);
                $.get("https://api.census.gov/data/2013/language?get=EST,LANLABEL,NAME&for=state:" + abvMap[key] + "&LAN=625", function(data) {
                    $("#missing-states-list").append("<li><div class=\"tooltip\">" + key + "<span class=\"tooltiptext\">" + data[1][0] + " spanish speakers</span></div></li>");
                });
            }
        }
    }
}

The details of this file is irrelevant. The issue here is in the for-loop, where every key in the dictionary abvMap is being looped through. states is an global array that contains states that have already been found, e.g. states = ["Maryland", "Texas"]. abvMap is a dictionary that contains all 51 states. I'm checking in each iteration of the for-loop of the key state have already been found. If not, I make an API call and append that state (and some data from the API) to the list #missing-states-list.

Judging by the console.log() outputs. There's absolutely no issue with states or key, it perfectly loops through every state in the dictionary. Even the API calls are correct. However, what's appended to the #missing-states-list is always Wyoming, which is the last entry in abvMap. I have idea why.

bli00
  • 2,215
  • 2
  • 19
  • 46

3 Answers3

2

Cause of the problem: lack of understanding scope

Check this example to understand the problem:

var creates function scope

var funcs = []

for (var i = 0; i < 10; i++) {
  funcs.push(function() {
    console.log(i)
  })
}

funcs.forEach(function(func) {
  func()
})

While you might expect this forEach loop to result in number 0 to 9 being printed, instead you get ten times 10. The cause of this is the variable i being declared using var keyword, which creates a function scope that leads to each function in funcs holding a reference to the same i variable. At the time the forEach loop is executed, the previous for-loop has ended and i holds 10 (9++ from the last iteration).

Compare how ES6's let, which creates block scope instead of function scope, behaves in this regard:

let (ES6 or officially ES2015) creates block scope:

var funcs = []

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

funcs.forEach(function(func) {
  func()
})

Because let creates block scope, each iteration of the for loop has its "own" variable i.

ES5 solution using an IIFE wrapper

If you need an ES5 solution, an IIFE (immediately invoked function expression) wrapper would be the way to go:

var funcs = []

for (var i = 0; i < 10; i++) {
  funcs.push((function(value) {
    return function() {
      console.log(value)
    }
  }(i)))
}

funcs.forEach(function(func) {
  func()
})

Here, i is passed as a parameter to each function which stores its own copy value.

The same is true for for..in loops:

var funcs = [],
  obj = {
    first: "first",
    last: "last",
    always: "always"
  }
  
for (var key in obj) {
  funcs.push(function() {
    console.log(key)
  })
}

funcs.forEach(function(func) { // outputs: "always", "always", "always"
  func()
})

Again, all functions in funcs hold the reference to the same key because var key creates a function scope that lives outside of the for..in loop. And again, let produces the result you'd probably rather expect:

var funcs = [],
  obj = {
    first: "first",
    last: "last",
    always: "always"
  }
  
for (let key in obj) {
  funcs.push(function() {
    console.log(key)
  })
}

funcs.forEach(function(func) {
  func()
})

Also compare the excellent (!) book

Nicholas C. Zakas: "Understanding ES6", no starch press, p. 8-9.

from which the examples were taken.

connexo
  • 53,704
  • 14
  • 91
  • 128
  • Thanks @connexo. I knew that `let` had tighter scoping than var but didn't know that it worked this way in a loop. Thanks for the pointer. – Raith Feb 24 '18 at 00:55
  • Feel free to upvote if you feel this is a useful answer. – connexo Feb 24 '18 at 00:58
1

It's because the $.get is asynchronous and by the time it returns, key is always at the end of the abvMap loop.

You will need to create an artificial closure around your $.get call so that the key value, correct as of the time of calling, is maintained.

(function (keysafe) {
    $.get("https://api.census.gov/data/2013/language?get=EST,LANLABEL,NAME&for=state:" + abvMap[keysafe] + "&LAN=625", function(data) {
    $("#missing-states-list").append("<li><div class=\"tooltip\">" + keysafe + "<span class=\"tooltiptext\">" + data[1][0] + " spanish speakers</span></div></li>");
    });
}(key));
Raith
  • 528
  • 3
  • 8
1

Sometimes using "var" will only display the last item in the list. To retain each item/element, you need to use "let" so it can rebind to each iteration (ES6).

How about just switching "var" to "let" in the for loop?

for(let key in abvMap) { ...
}

Christian Hur
  • 429
  • 3
  • 10