This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
Closed 5 years ago.
I am trying to load data from a csv and store it in an array of objects. I know global variables are frowned upon but I can't think of a better way to store the data and access it from multiple functions.
Here is my code:
var mydata = new Array;
$(document).ready( function () {
$.get('./datafile.csv', function(data) {
var head = data.split("\n");
for(var i = 1; i < head.length; i++){
line = head[i].split(",");
var obj = {
index:i,
img:line[0],
caption:line[1],
desc:line[2]
};
mydata.push(obj);
}
console.log(mydata); //1
});
console.log(mydata); //2
//I then want to select various elements on my page and set some attributes to
//an object in my data, but I can't since everything is undefined
});
At the first spot it logs my data correctly, but at second spot it logs an empty array. I read this article on global variables in JavaScript so I'm not sure what is going wrong.
The second part (//2) runs too soon. When $.get executes, it just starts the HTTP request to get the CSV, but doesn't wait for it to finish - that's why you need to pass that function(data) in. After the request finishes, the callback function gets called, and it's there that you should continue your initialization.
So, your code should look something like that below. (if you need to use the data elsewhere, you can keep on using the global, but it's not needed just for this)
$(document).ready( function () {
$.get('./datafile.csv', function(data) {
var mydata = [];
var head = data.split("\n");
// ...
console.log(mydata); //1
continueSetup(mydata); // 2
});
});
function continueSetup(mydata) {
// do what you need
}
I think you might be getting confused about the order of what is happening in your code. First of all, there is nothing wrong with using a global variable like this, especially if you are accessing it multiple times throughout your page (using events and such). Secondly, the reason you are seeing an empty array at your "second" spot in code is because that spot (#2) is actually getting executed before your get function has received the data and before #1.
get is an asynchronous function, which means that it waits to receive a response, and then executes the code inside (including #1). However, #2 gets executed immediately, while your array is still empty.
At 2 the data will be same as what you initialized. At 1 the data will be the same as what you populated.
2 gets printed first if you have observed closely. This is because $.get is an asynchronous call and gets executed in the background. The callback you are providing to $.get will run after the GET request is either successfully completed or errored out.
Related
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
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 in 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 on.Moreover the loop is so incredibly fast 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 declare 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: These are called self-executing functions and there is nothing special about them just a shorthand version of the code below
function hello(url,name){ //line #39
//your code
} //ln #53
hello(URL,offlineName); //ln #54
See this you'd find that it runs perfectly but the moment you comment out the function (line no. 39,53,54) it again reverts to the old bugged behavior.You might wonder how could a simple function change the behaviors so drastically.Here is how - it's all based on scope chains
Just like Java the JS interpreter(referred as VM hereafter) reads your code line by line now when it reaches hello's definition it just reads it (studies parameters,return and inside code) then moves on ; now it has reached the call hello(URL,offlineName); it runs the code inside hello but then it realizes that getJson has a callback which can't be 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 loop iterations URL and offlineName are reinitialized/assigned new values , they don't affect the values bound in [1] as they have no relation with them , they are totally different entities.This is because JS passes parameters by value(at least for primitive types)
But the most important thing about scope chains is that even after the loop gets over the values referenced in getJson callback are still there only thing is you can't access them directly but VM can .The reason is - the last function in the chain is a callback (recorded in list) and so to make any sense VM must let survive the values needed by it when it runs in the future , nerds call it a closure where inner function will always have access to things present in outer function even thought outer function call is over and control has returned somewhere else.Note that even in your earlier bugged code values were getting saved only problem was they were getting overwritten because for all of them had only one outer function ie loadStreams but when you create and call separate hellos each one creates a separate environment(something like a parallel universe).
In essence it creates scope chains so each iteration can have it's "Own space" where it's free from interference by others.
for loop --> hello() --> getJson's inner function (per iteration)
You might go well with let but first have a look at compatibility chart at http://caniuse.com/#feat=let
This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
Closed 6 years ago.
I have been stuck for far too long on the following problem that I really need to consider my theoretical knowledge about variable scope and callback functions in Javascript. Rather than a quick fix for my particular problem a generalized answer that tries to explain the theory behind my problem is preferred. Here is the code (that doesn't work) (oh and it uses jQuery's $.getJSON).
function getStreamerStatus(streamer) {
var channelurl = "https://api.twitch.tv/kraken/channels/";
var streamurl = "https://api.twitch.tv/kraken/streams/";
var temporary = {
status: "",
game: "",
picture: "",
name: streamer,
link: "https://www.twitch.tv/" + streamer
};
$.getJSON(streamurl + streamer, createCallback(temporary));
$.getJSON(channelurl + streamer, createCallback(temporary));
return temporary;
}
After some searching I used the "createCallback()" function in an attempt to make the "temporary" object visible to the callback function.
function createCallback(tmpobj) {
return function(json) {
//get's some information and stores it in the object passed as tmpobj
filterOut(json, tmpobj);
};
}
And in the end in the main function I have an array with names of twitch streamers and for each name the "getStreamerStatus()" function is called and the returned object is stored in an array.
function TwitchInit() {
var channels = [/*filled with strings of streamer names*/];
var response = []; //array with objects with all the information
for(var i = 0; i < channels.length; i++) {
response.push(getStreamerStatus(channels[i]));
}
parseContent(response); //not relevant for now
//for debugging
for(var j = 0; j < response.length; j++) {
console.log("--responseArray with Objects--");
for(var prop in response[j]) {
if(response[j].hasOwnProperty(prop)) {
console.log(prop + ": " + response[j][prop]);
}
}
}
}
The problem here is that if I log the objects to the console only the "link" and "name" properties of the objects have some content and the other properties who're supposed to be written by the "filterOut()" function always remain empty. According to the console the communication with the Twitch server is fine and I can read the response header/object from the console so that rules out. And since the "name" and "link" properties are also different at each log to the console that means that each time a new object is created which is fine. That leads me to the conclusion that the "temporary" object still somehow isn't visible to the callback function inside $.getJSON despite my attempt with "createCallback()" to make the object visible to the "filterOut()" function. I have found a lot of information about variable scope in JavaScript but so far it hasn't helped my to solve my problem. I have no clue what I am doing wrong here and I'm starting to get frustrated. I really hope someone can enlighten me here.
I think there is no problem is closure here, the only problem is that your getStreamerStatus function will perform async tasks but will return a value directly, and use it without waiting the async calls (getJSON) to complete.
Try to put you debug logs inside a setTimeout with few seconds delay ;)
To so things better you should rewrite your getStreamerStatus to only returns the data after the getJSON calls are done, not before, with a callback as parameter, or by returning a promise.
Or you should have something (ie: an event) to tell your function that the calls are finished, and that it can process the results.
I'm using Jquery's $.getJSON to get remote JSON from a server. The data is returned and stored in a global variable. The reason this variable is global is because I want that variable and it's data inside, to be the value of another variable outside of that function. So far, the variable with the global variable containing the JSON data, as it's value, has been "undefined" due to the rest of the code not waiting on the JSON. So I think a deferred object will help with this issue however I'm new to this idea. Here is my code below. How can I use a deferred object to allow my code to wait for the JSON data then define my variable outside of the function with the global variable containing the JSON data.
function getStuff(num, onContents){
$.getJSON('http://whateverorigin.org/get?url=' + encodeURIComponent('http://catholic.com/api-radio/' + num) + '&callback=?', function(data){
//call the oncontents callback instead of returning
onContents(data.contents);
});
}
//when calling getstuff, pass a function that tells what to do with the contents
getStuff(6387, function(contents){
window.GlobalVar = contents;
});
var NewVar = window.GlobalVar;
alert(NewVar);
It is obvious when reviewing our back-and-forth that there was a lot of confusion in our correspondence. Allow me to sum up:
First, you wanted to define a global variable with the result of your $.getJSON call. Your code was already doing this.
getStuff(6387, function(contents){
// the following statement will assign the value of 'contents' to the global variable 'GlobalVar'
window.GlobalVar = contents;
});
I was focussed on the last two lines of your snippet because they were breaking your code:
var NewVar = window.GlobalVar;
alert(NewVar);
The reason this doesn't work is because window.GlobalVar has not been defined yet. Although the line of code where window.GlobalVar is assigned the value of contents precedes the final two lines in the code, the evaluation of these lines is out of sync because the $.getJSON call is asynchronous (which basically means it has its own timeline). So, window.GlobalVar = contents; has not been executed when var NewVar = window.GlobalVar; is attempted - so the line fails.
Due to the asynchronism, any code that relies on contents has to be executed after the $.getJSON call - which is what you hinted at in your original question when you mentioned "deferreds". This is why I urged you to move your last two lines into your callback. Using a deferred would not have been essentially different from what you were already achieving with your callback - a deferred just allows you to attach multiple callbacks that fire on different events.
We finally established that your issue is specific to Angular JS - that is, how to attach ajax data to your $scope. Whereas using a jQuery deferred would not help you, I think using an Angular deferred will. What you need to do is to replace jQuery's getJSON method with the jsonp method from Angular's http service - this will allow you to attach a 'success' handler which will update your $scope (and your view) when your ajax data arrives:
function getStuff(num) {
$http.jsonp('http://whateverorigin.org/get?url=' + encodeURIComponent('http://catholic.com/api-radio/' + num) + '&callback=JSON_CALLBACK')
.success(function (contents) {
$scope.shows = contents;
});
};
getStuff(6387);
Our ajax response contains an object with a key of 'contents' and a value which is a string of json data, within which is the 'shows' data we want. I'm going to modify the .success handler so that we can get the 'shows' data by deserializing the json string:
.success(function (response) {
var result = angular.fromJson(response.contents);
$scope.shows = result.shows;
});
Next, in our Angular, we want to iterate over each 'show' in 'shows' and we can do this using the ng-repeat directive:
<ul>
<li ng-repeat="show in shows">{{ show.title }}</li>
</ul>
I want to save the value of data and status in a variable and use it after the closing brackets of jquery GET/POST function.But alert comes only when it is inside .get braces.
$(document).ready(function(){
$.get("demo_test.asp",function(data,status){
v = data;
});
alert("Data:"+v);
});
As Jasper said, your alert is being triggered before the request is complete (async!). So, you have two options:
Do your logic inside the callback:
$.get("demo_test.asp",function(data,status){
v = data;
alert("Data:"+v);
//Process stuff here
});
Or pass the received data onto another function and work with it there
$.get("demo_test.asp",function(data,status){
v = data;
doStuff(v);
});
function doStuff(param) {
console.log(param);
}
You're absolutely correct; the code is working as it should... here's why:
The page loads and starts running code, it then hits the .get command and then keeps running, obviously making it to the 'alert' you have next. Since the .get function is still working on fetching the data before your page makes it to the 'alert' part... there's nothing to prompt.
You might want to string things together after the .get, using deferred objects. Look into: http://api.jquery.com/deferred.always/
This is a way of tacking on another function inside of the one fetching your data, so they depend on each other.
Simple answer, yes, you can store the data in a global variable and access it elsewhere. However, you must wait until it is ready.
The better way to do it is to instead store the jqXHR globally and simply add a done callback to it when you need to access the data.
var reqDemoTest = $.get(...);
//... some time later...
reqDemoTest.done(function(data){
console.log(data);
});
Can someone tell me why this alert is empty?
var pending_dates = [];
$.getJSON('/ajax/event-json-output.php', function(data) {
$.each(data, function(key, val) {
pending_dates.push({'event_date' : val.event_date});
});
});
alert(pending_dates);
I can't get my head around this. Am I not declaring pending_dates as a global variable, accessible within the each loop? How would one solve this?
Please note that the JSON output is working well. If I would declare pending dates within the getJSON function (and alert within that function), it works, but I need to store the data in an array outside of that getJSON function.
Thanks for your contributions.
EDIT
Thanks to your comments this code is working:
pending_dates = [];
$.getJSON('/ajax/event-json-output.php', function(data) {
$.each(data, function(key, val) {
pending_dates.push({'event_date' : val.event_date});
});
}).success(function() { alert(pending_dates); })
Thanks a lot for your contributions!
I think the problem is $.getJSON is an asynchronous call - it returns immediately and then alert(pending_dates) is invoked.
However, when that happens, the response from the asynchronous call may not have been received, and hence pending_dates may not have been populated.
That is probably why it is empty at the time alert(pending_dates) is called.
Your alert is executing before the JSON call has finished. Remember this JSON it fetched and processed asynchronously, but your alert comes right after it's started. If you want an alert, then you need to put it at the completion of the getJSON call.
$.getJSON is working asynchronously meaning that whatever you have specified in the callback will be executed eventually but there is no guarantee that will happen by the time you reach alert('pending_dates').
You can verify this by moving alert('pending_dates') right after
pending_dates.push() (this would result in one alert being displayed for each item it is retrieving).
You can write a function to start working with the data you're retrieving as soon as it is available:
var pending_dates = [];
$.getJSON('/ajax/event-json-output.php', function(data) {
$.each(data, function(key, val) {
pending_dates.push({'event_date' : val.event_date});
doSomething(val.event_date);
});
});
function doSomething(date) {
// do something with date
// like writing it to the page
// or displaying an alert
}
With this you'll be able to work with all the data you get as it becomes available.
Variables are global by default in Javascript - having var actually introduces scope. Remove that and see if it helps.
It's more likely that the AJAX response isn't returning any data. Can you try pushing just 'foo' onto the array, and see if the alert shows anything different?