Understanding variable scope and closures with JavaScript callback functions [duplicate] - javascript

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.

Related

Scope of global array javascript [duplicate]

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.

variable value not being stored, does js compile code as it is written? [duplicate]

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 am trying to do a simple task. here is my function i am taking in a url link as parameters. i read the link, scrap it for data like title and headers. then i just try to store the data in mongo.
for some reason looking at this code console prints y first and then x.
why is javascript not compiling the code as it is written?
any help on how i may be able to store this data in a global var.
thanks
i believe it is a call back error..
insertNewItem(link){
check(link, String);
var xray = new Xray();
var tname;
xray(link, 'title')(function(err, title) {
tname = title;
console.log('x', tname);
});
var header;
xray(link, 'h1')(function(err, h1) {
header = h1;
});
console.log('y',tname);
Items.insert({
url: link,
name: tname,
bio: header
});
}
Your code snippet is a bit short, but it appears that the function returned from xray executes its callback asynchronously. This means that your function will run to completion, then your callback (containing the console.log('x'...) will be executed.
This works much like setTimeout:
setTimeout(function() {
console.log('a');
}, 1)
console.log('b');
// Prints b, a
Whenever you use an async callback, you must keep in mind that your code will not be executed top-to-bottom.
It takes a little to wrap you head around, but you catch on eventually.
In your case, you will probably want to do something like:
var tname;
var i = 0
xray(link, 'title')(function(err, title) {
tname = title;
console.log('x', tname);
insert();
});
var header;
xray(link, 'h1')(function(err, h1) {
header = h1;
insert();
});
console.log('y', tname);
function insert() {
if (i = 1) Items.insert({
url: link,
name: tname,
bio: header
});
else i++;
}
That will start xray running, and call Items.insert when all the data is available. It would be a bit cleaner with promises, but that's a whole different subject.

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
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

Why does it work correct with an array but not with an object?

I am developing an AngularJS application and found the following behavior.
I have two functions in my service. The first function returns all the categories stored in the database and the second returns one category by its id.
Here is my service:
angular.module('categoriesRepository', [])
.service('categoriesRepository', ['$cordovaSQLite', 'sqliteHelper',
function ($cordovaSQLite, sqliteHelper) {
//this works - returns an array with all categories
this.getAll = function () {
var categories = [];
$cordovaSQLite.execute(sqliteHelper.getDb(),
"SELECT * FROM categories;")
.then(function (res) {
for (var i = 0; i < res.rows.length; i++) {
categories.push(res.rows[i]);
}
});
return categories;
}
//this works not - returns undefined
this.getById = function (id) {
var category;
$cordovaSQLite.execute(sqliteHelper.getDb(),
"SELECT * FROM categories WHERE id = ?;", [id])
.then(function (res) {
category = res.rows[0];
});
return category;
}
}]);
I know that I can use Angulars $q to run functions asynchronously, and use their values when they are done processing.
Why does the getById function return the category directly and the getAll wait until the array is filled?
EDIT
I had the getAll function posted wrong. There is no return statement before $cordovaSQLite.execute
UPDATE:-
After your question is updated.
In the first example your are creating an array first by doing var categories = [];and then returning this array before finishing your async call. When your async call completes it just pushes certain elements into the array thus not destroying the reference to the array (categories ) variable. When it is returned back if you will debug it you will find the function returning an empty array and later when the async call succeeds only then the array will be filled.
In the second example you are creating just a variable and then returning it before the async call finishes. But then the async call is finished you assign the variable to a new value. thus destroying the earlier reference.
Solution:-
Though not a preffered approach to make it work. you will have to maintain the category variable reference. for this you can use angular.copy OR angular extend
So the second part of your code should be like
this.getById = function (id) {
var category;
$cordovaSQLite.execute(sqliteHelper.getDb(),
"SELECT * FROM categories WHERE id = ?;", [id])
.then(function (res) {
angular.copy(res.rows[0], category);
//now the reference to the category variable
//will not be lost
});
return category;
}
Better Practice:-
The way you have been developing this application is wrong. Async calls should not be handled this way. I earlier asked a question just to clarify the way to handle the async calls and state inside the angular app, factories and controllers please have a look here. It provides two ways to handle the state and async calls. There might be many more practices out there but these two suit me best.
It is unfortunate that this approach appears to 'work' because it is caused by the modification of the returned array object "at some unspecified time" after it is returned.
In the usage the array is accessed/observed after1 it has been modified by the asynchronous call. This makes it appear to function correctly only because of the (accidental) asynchronous-later-than observation.
If the observation was prior to the actual completion of the SQLite operation - such as immediately after the getAll function call - it would reveal an empty array.
Both functions are incorrectly written and the first accidently releases Zalgo (or perhaps his sibling).
See How do I return the response from an asynchronous call? for more details.
1 Chrome's console.log can be confusing as it works like console.dir and thus may be showing the current value and not the value when it was invoked.
As stated already, this is a bad approach. You can't use result of your function immediately after it returns.
However I didn't see the answer to your exact question: why do they behave differently?
It happens because with an array you return a reference to an object (type Array). Later on you use same reference to modify contents of the object, i.e. push new items into the array.
However in second function you modify the reference itself. You make you local variable categories point to a new object. Thus old object (reference to which was returned to outer scope) remains untouched. To make it work the same way you should have written
category.row = res.rows[0];
You return the result of the execute in the first case, whereas you return the variable in the second case, which has most likely not been populated yet.

Javascript function returns undefined [duplicate]

This question already has answers here:
How to return value from an asynchronous callback function? [duplicate]
(3 answers)
Closed 9 years ago.
First off I am new to javascript and this is my first 'project' in this language, so I apologize for my noobiness in advance.
I am using leaflet and D3 in this project and I can't seem to get this function to return anything except for 'Undefined'. At first I though I wasn't returning from the function properly so I tried to duplicate the error on a smaller scale here:
http://jsfiddle.net/KhqwR/
However, that worked for me so now I am a little lost on what to do.
Here is a simple version of my code, I tried to remove everything that didn't seem relevant and changed the names to make it easier to understand:
$(function () {
...
function getThings(code) {
d3.csv("data.csv", function(data){
for (var i = 0, len = data.length; i < len; i++){
if (data[i].code == code){
alert("return 5!")
return 5;
}
else{
return 0;
}
}
})
}
L.geoJson( features, {
style: function (feature) {
return { opacity: 0, fillOpacity: 0.5, fillColor: "#0f0" };
},
onEachFeature: function(feature, layer){
var test = getThings(5);
alert(test);
...
I consistently get to the "return 5!" alert and then at the alert(test) I just get "Undefined".
Does anybody know where I went wrong?
Thanks in advance!
d3.csv actually returns a useful value that can be used for attaching relevant callbacks. However, because the getThings doesn't have a return statement, then it will always yield undefined when invoked.
Remember that return applies to the nearest enclosing function, including anonymous functions such as the provided callback.
function getThings(code) {
/* Without an explicit return, a function always evaluates
to undefined when invoked. */
return d3.csv("data.csv", function(data){ .. });
})
var test = getThings(..);
/* Now test is NOT undefined, but is also NOT the data
for the asynchronous reasons discussed elsewhere.
See the link above for correct usage. */
test.row(..);
d3.csv() is async. Which means, the function getThings() kicks off d3.csv() and instantly returns undefined
From d3 documentation:
Issues an HTTP GET request for the comma-separated values (CSV) file
at the specified url. The file contents are assumed to be
RFC4180-compliant. The mime type of the request will be "text/csv".
The request is processed asynchronously, such that this method returns
immediately after opening the request.
This is happening due to Javascript Async Nature. Use function Callback.

Categories

Resources