Thinh Cao Thinh Cao - 22 days ago 5
Javascript Question

Variable value overwritten in getJSON function

I am working on a challenge in which I have to display Twitch channels that are offline and online. Here is the function that is having a bug:

function loadStreams(){
for (var i = 0; i < channel_list.length; i++){
offlineName = channel_list[i];
console.log("offline name is: " + offlineName);
URL = "https://wind-bow.hyperdev.space/twitch-api/streams/" + channel_list[i] + "?callback=?";
$.getJSON(URL, function(data){
console.log("Now offline name is: " + offlineName);
console.log(data);
if (data.stream !== null){
currChannel = new Channel(data.stream.channel.display_name, data.stream.channel.status);
}
else {
currChannel = new Channel(offlineName, "Offline");
}
outArr.push(currChannel);
});
}
//showAll();
}


channe_list is an array of string preloaded with channel names. It is defined as follows:

var channel_list = ["ESL_SC2", "OgamingSC2", "cretetion", "freecodecamp", "storbeck", "habathcx", "RobotCaleb", "noobs2ninjas"];


My code is simply go through the channel_list, fetch the JSON data, and return the result and create a new instance of the Channel object, defined as:

var Channel = function(name, status){
this.name = name;
this.status = status;
}


For some reasons, in my else block, the variable "offlineName" is ALWAYS overwritten by the last value of the channel_list array, 'noobs2ninjas'. In other words, when I create an instance of the Channel class in the else block, "offlineName" is always "noobs2ninjas". Please let me know what i am doing wrong here. Here is my CodePen if you would like to take a look at the whole thing:

https://codepen.io/tcao2/pen/XNbbbm?editors=1010

Answer

Here is your issue

You might be aware of how fast a for loop runs (well it's very fast) but your network isn't fast at all when compared to and this is what causes you problem.Let me explain

You are using $.getJSON with URL which has its value depend on offlineName but you are using offlineName i your success callback too.Now suppose for first req. offlineName is "ESL_SC2" now ajax request uses it in URL but as usual due to network latency the response doesn't arrive instantly meanwhile loop is now on second iteration .BUT wait offlineName IS "OgamingSC2" now!! and will be used so when your success callback of first request completes but wait there are even more entries so even "OgamingSC2" would get vanquished later.Moreover the loop is so incredibly faster that by the time 1st or 2nd response comes in your loop is already at its last iteration so only final offlineName value (noobs2ninjas) survives which is then used in success callback of all others.

Solution: The solution is to find some way by which each iteration would preserve its offlineName value and use the same in its corresponding success callback.The simplest way is to use let to decalare URL and offlineName which limits the scope per iteration it essence provide an effect similar to a closure

https://codepen.io/vsk/pen/LbNpBQ

Only problem with the above code is that let is a recent addition and older browsers don't support it well , so the other solution would be to actually implement a closure per request passing URL and offlineName

(function(url,name) {
   $.getJSON(url, function(data){
   if (data.stream !== null){
     currChannel = new Channel(data.stream.channel.display_name, data.stream.channel.status);
   }
   else {
     currChannel = new Channel(name, "Offline");
   }       
   outArr.push(currChannel);
  }); 
})(URL,offlineName);

https://codepen.io/vsk/pen/rWeOGL

EDIT:It's called self-executing function it isn't a closure in traditional sense(doesn't return a function) but rather a simple function there is nothing special about it's just a shorthand version of below code

function hello(url,name){                //line #39
  //your code                           
}                                        //ln #53
hello(URL,offlineName);                  //ln #54

In-fact see this https://codepen.io/vsk/pen/dOMVKX you will see it runs perfect but the moment comment out the function ( line no. 39,53,54) it again reverts to the old behavior of returning only "noobs2ninjas".You might wonder how can a simple function change the behaviors so drastically.Here i explain how

Just like java ,interpreter in JS reads your code line by line now when it reaches hello's definition it just reads it(studies parameters,return and inside code) moves on ; now it has reached the call hello(URL,offlineName); it runs the code inside hello but then it realizes that get json has a callback which can't called at this moment so it records this in it's "to be called later" list along with the values of all variable used in that function at that time [1].So even if in later iterations URL and offlineName are reinitialized/assigned new values , they don't have any relation to values bound in [1].This is because JS passes by value, see example below

function greetings(name){ alert("greetings from " + name); name="Hulk";
  //"name" in parameter is just a value has no association with global "avenger" with value Thor, interpreter just copied "loki" on it and gave us to use in alert, nothing would have changed if we had "address" instead of "name" there
}
var avenger = "Thor";
greetings("loki")    //greetings from loki
console.log(avenger) //Thor

Note that it's very different from time record ie. interpreter never maintains a table of values of URL and offlineName at various time rather it just discards them as soon as their work is done :( yet their values are passed function to function and since the last function in the chain is a callback it survives and so to make any sense interpreter must let survive any parameters and inner variables that it might need when run in future.

You might go well with let but first have a look at compatibility chart at http://caniuse.com/#feat=let

Comments