Variable value overwritten in getJSON function - javascript

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

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.

Callbacks usage with instanced objects in Javascript

I was facing a problem with callbacks in Javascript. I solved my problem using what I would call an ugly property of Javascript (so to say, something that would logically be forbiden and never work in other languages than Javascript). So my question: Is there an ELEGANT way, to do the same thing.
I will so begin with the beginning. My goal was to wrap, in some manner, the Web Audio API. In the architecture, I implemented a class, lets call it AudioRessource, which is destined to be an interface (abstraction) in some manner of the AudioBuffer object of the Web Audio API.
This class (AudioRessource) have a prototype member function that must simply take an url as argument to automatically load audio data, decode it, handle errors, etc and finally hold the resulting AudioBuffer object in a "pseudo-private" member:
function AudioRessource()
{
this._aBuffer = null; // future reference to `AudioBuffer` object
this._loadStatus = 2;
};
AudioRessource.prototype.loadData = function(url) {
/* deal here with async functions to
provides audio data loading automation */
}
The main problem here, is that this will be an object instance (of AudioRessource) which will create the callback functions, using only local references, and must be able to pass the final AudioBuffer object to itself.
To load the raw audio data, this is pretty simple, I use the XMLHttpRequest object, with an extra property set as member of the XMLHttpRequest object, like this:
AudioRessource.prototype.loadData = function(url) {
let req = new XMLHttpRequest();
req.extraProperty = this; // reference to `AudioRessource` instance
req.onload = function(){
// retrive instance reference within the callback
this.extraProperty._loadStatus = 0;
}
req.onerror = function(){
// retrive instance reference within the callback
this.extraProperty._loadStatus = -1;
}
req.open('GET', url, true);
req.send(null);
this._loadStatus = 1;
}
The big problem appear when we have to decode the coded raw audio data into PCM data, that is, an Web Audio API AudioBuffer object instance. Indeed, the Web Audio API provides only one function to achieve this, and this function is asynchronous, and takes a callback that simply recieve the resulting buffer as argument: how to "catch" this resulting buffer to assign it to the proper AudioRessource instance (the one who lauched the process) ? This work that way:
AudioCtx.decodeAudioData(rawData,
function(result){
// do something with result },
function(error){
// do something with error });
My first naive approach, was to think like we were in C/C++ : I simply put an AudioRessource instance function "pointer" (reference) as callback, this way, the AudioRessource instance will directly recieve the buffer:
// where 'this' is an `AudioRessource` instance
AudioCtx.decodeAudioData(rawData,
this._handleDecodeSuccess,
this._handleDecodeError);
However, this does not work, because in javascript, this is not a "function pointer" that is passed into the decodeAudioData, but if I well undstand, an literal expression, that is, the "ASCII content" of the function... So the 'this' reference is lost !
I spent some time to try understand how this kind of asynchronous function is attended to work, since to me, coming from C/C++, this is simply an heresy: The function does not take any extra argument, no way to pass any external reference... "What is that thing ?". Then I finaly decided to try the "Illogical Javascript logic" way... And I found the solution :
// Create local variable which stores reference to 'this'
let thisInstReference = this;
// Use the local variable to write our callback
AudioCtx.decodeAudioData(rawData,
function(resut){
thisInstReference._aBuffer = result;
thisInstReference._loadStatus = 0;
},
function(resut){
thisInstReference._loadStatus = -3;
});
To be honnest, to me, this is simply freaking. First of all, I even don't understand what realy happen: HOW a local variable (to a object instance's member function), that stores a reference to an object instance (this), can be used "as this" in a callback function ? I do not even understand how a language can allow this kind of thing. Secondly, to me, this not a "proper way" to code something: this code is simply illogical, dirty, this works but this appear as an ugly hack that takes advantage of Javascript misdesign.
So here is my question: How to achieve this, in a elegant way ?
Your problem is simply due the the nature of how this works in javascript. The value of this is not bound at compile time nor at runtime but instead very late at call time.
In the following code:
AudioCtx.decodeAudioData(rawData,
this._handleDecodeSuccess,
this._handleDecodeError);
.. the value of this inside _handleDecodeSuccess and _handleDecodeError is not determined at object creation time but instead at the time they are called. And it is the decodeAudioData method that will eventually call them when decoding is complete. This causes the value of this to become something else (depending on how the functions are called).
The modern solution is to statically bind this to the functions:
AudioCtx.decodeAudioData(rawData,
this._handleDecodeSuccess.bind(this),
this._handleDecodeError.bind(this));
Note: the .bind() method creates a new function that wraps your function with this permanently bound to the argument you pass to it.
The traditional solution is to capture this inside a closure like what you have done.

Passing a variable to a request-promise function inside a forEach loop

I am calling an API for price data which is working correctly. However, I am trying to pass the variable exchange_pair_id into the then() function.
Inside the forEach loop the exchange_pair_id is correct for each asset. However inside the then() function this variable is always the same (the last value in the forEach loop).
I am trying to pass the exchange_pair_id variable into then(function(response) {...}
response.forEach(function(asset) {
var assets = asset['assets'];
exchange_pair_id = asset['exchange_pair_id'];
options.uri = exchange_objects[exchange[0]].ticker + '?pair=' + assets[0] + assets[1]; // overwrite to fit the request to Kraken API
console.log(exchange_pair_id) // unique
rp(options).then(function(response) {
key = Object.keys(response['result']);
price_data = response['result'][key];
console.log(exchange_pair_id) // duplicate
});
});
This happens due to the fact, that the function in the .then() is executed after the request-promise resolves, which takes some time. In the meanwhile the forEach-loop finishes and assigns the last asset to exchange_pair_id - which in your code is a global variable as a proper declaration is missing (see below)! After that the first request-promises begin to resolve and execute their callback, but at this time the forEach-loop is already finished so exchange_pair_id will always log the same value.
To avoid that exchange_pair_id is in the global scope, you should use a , instead of ; after var assets = asset['assets']; (second line). Or simply add another var in front of exchange_pair_id = asset['exchange_pair_id'];
response.forEach(function(asset) {
var assets = asset['assets'], // <---- use ',' instead of ';'
exchange_pair_id = asset['exchange_pair_id'];
options.uri = exchange_objects[exchange[0]].ticker + '?pair=' + assets[0] + assets[1]; // overwrite to fit the request to Kraken API
console.log(exchange_pair_id) // unique
rp(options).then(function(response) {
key = Object.keys(response['result']);
price_data = response['result'][key];
console.log(exchange_pair_id) // duplicate
});
});
Declared variables are constrained in the execution context in which they are declared. Undeclared variables are always global.
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var)
Anyway I recommend you to use let in order to declare variables as it may sometimes prevent undesirable behaviour of your code. See the MDN docs on let to get to know what it does - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
Here you are experiencing an asynchronous problem.
When you execute the forEach you push tasks into the node.js to do list.
Then when you function stop, they get executed and access id.

Deferred timing issue

I have the following code I've designed to load and run script at runtime. You'll note that I save it to localStorage if it isn't already there. Now it runs fine if it's stored there already, but when it's just got the text from the file it throws ReferenceError: loginLaunch is not defined, though the text seems to have been loaded (hence the console.log lines that check the length). For your convenience I've included a line, localStorage.clear();, to make it alternate between the error message that's the problem and ReferenceError: loginLaunch is not defined, which given the code below is the desired result.
I don't understand why it should work one way and not the other. If it's a timing issue I don't see how the use of the promise, loginCode, lets it through unless possibly appendChild() is asynchronous, but I'm under the impression that it isn't (mainly because it has no callback, and I tried to find out, but could not) and even then why would code before the appendChild() have an impact?
Have I messed up one of the promises? I include the contents of the file login.js at the end. I searched SO for anything relevant but without any luck except for just one post that states that appendChild is synchronous.
Please help.
var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(loginLaunch());
//FUNCTIONS START HERE
function getCode(local, source) { //This creates the promise to get the code (not to run it)
console.log("start of loadCode");
dfd = $.Deferred(); //This is the one to return.
script = localStorage.getItem(local); //Try to load from local storage.
// console.log("script after local attempt: "+script);
if (script) { //If found...
console.log("found Local code");
dfd.resolve(script);
localStorage.clear(); //Added for debugging
} else { //load from file.
ajax = $.ajax({
url : source,
cache : false,
dataType : "text", //load as text initially so that we can store it locally.
});
ajax.done(function(fromFile){
localStorage.setItem(local, fromFile); //store it locally.
//console.log("script after ajax attempt: "+script);
dfd.resolve(fromFile);
});
ajax.fail(function(){
dfd.reject("Error retrieving code. You may be disconnected");
});
}
return dfd.promise();
}
function runCode(local, source) {
dfd = $.Deferred(); //This is the one to return.
code = getCode(local, source); //local promise
code.done(function(retrievedCode){
console.log(retrievedCode.length);
var head = document.getElementsByTagName('head')[0]; //first head section
var el = document.createElement("script"); //named the same as the local storage
//script.type= 'text/javascript'; Redundant — it's the default
// el.id = local; //Probably redundant, but if we want to manipulate it later...
el.text = retrievedCode;
head.appendChild(el); //This shouldn't run anything, just make global functions that will be called later.
console.log(el.text.length);
dfd.resolve(); //If we need to return the node to manipulate it later we'd make the variable above and 'return' it here
});
return dfd.promise();
}
Here's the contents of the login.js file.
function loginLaunch(){
dfd = $.Deferred(); //This is the one to return.
loadElement("login.1001", "#content", "login.html");
//After the element has been loaded we have a disconnect — i.e. there's no promise waiting, so we have to wait for the user.
}
$("#content").delegate('#loginButton','click',function(){
console.log("Login click");
//php to pick up the entered details and pass them to common php that also uses the
checkCredentials = $.ajax({
type : "POST",
url : "./php/credentials.php",
data : {
queryString : queryString
},
datatype : "text", // 1 or 0
});
checkCredentials.done(credentialsChecked(success));
// MOVE THIS STUFF
readyPublicList();
$.when(publicListCode,loggedIn).then(runDefaultPublicList()); //Assumes successful login so it loads the code for the list window in parallel.
//Note that it's probable that my approach to the login window may change, because it needs to be available on the fly too.
// $("#content").html("<p>test</p>"); //Successfully tested, well it was once.
});
function loginHide(){
$("#loginHtml").hide;
}
I'm not sure why this works:
var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(function(){loginLaunch();});
and this doesn't:
var loginCode = runCode("login_1001","./js/login.js");
loginCode.done(loginLaunch);
My one thought is that maybe if you pass literal named functions to .done then they are validated when loginCode is created, while anonymous functions aren't validated until they are about to be run.
I should note that the error was appearing before the console.log output.
Maybe someone with a better grasp of the technicalities can clarify. For now I'm just happy to stop tearing my hair out, but I like to know how things work...
You need to change at least three things. First change this:
loginCode.done(loginLaunch());
to this:
loginCode.done(function() {loginLaunch()});
You need to be passing a function reference to the .done() handler so it can be called later. The way you had it, you were calling it immediately BEFORE loginCode() was done with its work, thus it was getting called too early.
In addition, loginLaunch doesn't exist yet so you can't pass a reference directly to it. Instead, you can pass a reference to a wrapper function that then calls loginLaunch() only after it finally exists.
And second, you need to declare your local variables with var so they aren't implicit globals and stomp on each other. For example, you have multiple functions who call each other trying to use the same global dfd. That is a recipe for disaster. Put var in front of it to make it a local variable so it's unique to that scope.
And third, el.text doesn't look like the right property to me for your script. Perhaps you meant to use .textContent or since you have jQuery, you can do:
$(el).text(retrievedCode);
In a couple style-related issue, ALL local variables should be declared with var before them so they are not implicit globals. This will bite you hard by causing mysterious, hard to track down bugs, even more so with async code.
And, you can generally use the promise returned by jQuery from ajax functions rather than creating your own.
To incorporate those improvements:
runCode("login_1001","./js/login.js").done(loginLaunch);
function getCode(local, source) { //This creates the promise to get the code (not to run it)
var script = localStorage.getItem(local); //Try to load from local storage.
if (script) { //If found...
localStorage.clear(); //Added for debugging
// return a resolved promise (since there's no async here)
return $.Deferred().resolve(script);
} else { //load from file.
// return the ajax promise
return $.ajax({
url : source,
cache : false,
dataType : "text", //load as text initially so that we can store it locally.
}).then(function(fromFile){
localStorage.setItem(local, fromFile); //store it locally.
return fromFile;
});
}
}
function runCode(local, source) {
return getCode(local, source).then(function(retrievedCode){
console.log(retrievedCode.length);
var head = document.getElementsByTagName('head')[0]; //first head section
var el = document.createElement("script"); //named the same as the local storage
$(el).text(retrievedCode);
head.appendChild(el); //This shouldn't run anything, just make global functions that will be called later.
console.log(el.text.length);
});
}
FYI, if you just want to insert a script file, you don't have to manually retrieve the script with ajax yourself. You can use the src property on a script tag and let the browser do the loading for you. You can see a couple ways to do that here and here.

Working immediately with instances of asynchronous (dynamically loaded) classes in Javascript

The situation was that I wanted to create an instance of a helper class, but that helper class required initialisation through external scripts, so it was inherently asynchronous. With
var obj = new myObj();
clearly an call to
obj.myMethod();
would yield undefined, as obj would either be empty or undefined until its methods and params were loaded by the external script.
Yes, one could restructure things to have a callback pattern and work with the new object within that, but it gets cumbersome and awkward when working with a large and varied API with many dynamic objects as I've been working with.
My question has been, is there any possible way to cleverly get around this?
I imagine the academically trained programmers out there have a name for this sort of approach, but I put it here in case it's not better written somewhere.
What I've done is modify my loader class to use a placeholder+queue system to instantly return workable objects.
Here are the components. Sorry that there are jQuery bits mixed in, you can easily make this a pure-JS script but I've got it loaded anyway and I'm lazy.
'Client' makes this request, where 'caller' is my handler class:
var obj = caller.use('myObj',args);
In Caller, we have
Caller.prototype.use = function(objname,args) {
var _this = this;
var methods = ['method1','method2'];
var id = someRandomString();
this.myASyncLoader(objname,function(){
var q = [];
if (_this.objs[id].loadqueue) {
q = _this.objs[id].loadqueue;
}
_this.objs[id] = new myRemotelyLoadedClass(args);
//realise all our placeholder stuff is now gone, we kept the queue in 'q'
_this.objs[id].isloaded = true;
//once again, the jquery is unnecessary, sorry
$.each(q,function(a,b){
_this.objs[id][b['f']](b['a']);
});
});
_this.objs[id] = _this.createPlaceholderObj(methods,id);
return _this.objs[id];
}
This function basically initiates the loader function, and when that's done loads a new instance of the desired class. But in the meantime it immediately returns something, a placeholder object that we're going to load with all of our remotely loaded object's methods. In this example we have to explicitly declare them in an array which is a bit cumbersome but liveable, though I'm sure you can think of a better way to do it for your own purposes.
You see we're keeping both the temporary object and future object in a class-global array 'objs', associated with a random key.
Here's the createPlaceholderObj method:
Caller.prototype.createPlaceholderObj = function(methods,id) {
var _this = this;
var n = {};
n.tempid = id;
n.isloaded = false;
$.each(methods,function(a,methodCalled){
n[methodCalled] = function(){
_this.queueCall(id,methodCalled,arguments);
}
});
return n;
}
Here we're just loading up the new obj with the required methods, also storing the ID, which is important. We assign to the new methods a third function, queueCall, to which we pass the method called and any arguments it was sent with. Here's that method:
Caller.prototype.queueCall = function(id,methodName,args) {
if (this.objs[id].isloaded == true) {
this.objs[id][methodName](args);
} else {
if (this.objs[id].loadqueue) {
this.objs[id].loadqueue.push({'f':methodName,'a':args});
} else {
var arr = [{'f':methodName,'a':args}];
this.objs[id].loadqueue = arr;
}
}
}
This method will be called each time the client script is calling a method of our new object instance, whether its logic has actually been loaded or not. The IF statement here checks which is the case (isloaded is set to true in the caller method as soon as the async function is done). If the object is not loaded, the methodName and arguments are added to a queue array as a property of our placeholder. If it is loaded, then we can simply execute the method.
Back in the caller method, that last unexplained bit is where we check to see if there is a queue, and if there is, loop through it and execute the stored method names and arguments.
And that's it! Now I can do:
var obj = caller.use('myObj',args);
obj.someMethod('cool');
obj.anotherMethod('beans');
and while there might be a slight delay before those methods actually get executed, they'll run without complaint!
Not too short a solution, but if you're working on a big project you can just put this in one place and it will pay many dividends.
I'm hoping for some follow-ups to this question. I wonder, for example, how some of you would do this using a deferred-promise pattern? Or if there are any other ways? Or if anyone knows what this technique is called? Input from JS whizzes much appreciated.

Categories

Resources