Strange variable scope in javascript - javascript

So i wrote this piece of code in javascript :
// Additional initialization code such as adding Event Listeners goes here
FB.api('593959083958735/albums', function(response) {
if(!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
for(i=0; i<response.data.length; i++){
var albumName = response.data[i].name;
var albumCover = response.data[i].cover_photo;
var albumId = response.data[i].id;
console.log(albumName);
FB.api( albumCover, function(response) {
if(!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
$("ul").append('<li>'+
'<a href="testFile.HTML" data-transition="slidedown">'+
'<img src= "' + response.picture + '" />'+
'<h2>' + albumName + '</h2>'+
'<p> Test Paragraph</p>'+
'</a>'+
'</li>')
.listview('refresh');
}
});
}
}
});
I am using facebooks javascript API , to import the photo albums of a facebook page to my jquery mobile page and put them in a listView. As you see i create the listView dynamically. The listView has as thumbnail the albums coverPhoto and headline , albums name.
However something is horribly wrong here. The result of the code above can be seen here
The thumbnails are all correct but the album names are wrong. In every cell i get the last album name. When i console.log(albumName) in my code as you see , i get all the names correct. But inside the second call to FB.api the "albumName" variable holds only the last album name.
Any idea what is going on here? Its literally driving me nuts...

Looks like you have the famous i for loop problem where you only have a reference to i.
for (var i = 0; i < response.data.length; i++) { //added the var here
(function (i) { //created a function
var albumName = response.data[i].name;
var albumCover = response.data[i].cover_photo;
var albumId = response.data[i].id;
console.log(albumName);
FB.api(albumCover, function (response) {
if (!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
$("ul").append('<li>' +
'<a href="testFile.HTML" data-transition="slidedown">' +
'<img src= "' + response.picture + '" />' +
'<h2>' + albumName + '</h2>' +
'<p> Test Paragraph</p>' +
'</a>' +
'</li>')
.listview('refresh');
}
});
})(i); //call the function with i
}

It's actually not the i that's the problem, but rather albumName.
Most JavaScript engines are very forgiving and allow you to declare the same variable with the var keyword multiple times. That's what you're doing here. It's important to note that the for keyword does not introduce a new scope; so when you (re-)declare each variable within the loop (albumName, albumCover, and albumId) you're overwriting the value previously stored in the same variable.
This comes back to bite you because the callback you pass to FB.api closes over these local variables and (clearly) executes asynchronously. By the time the callback is actually run, the for loop has completed and these variables are all set to their last value.
The exception, of course, is albumCover, as you already observed (you said the thumbnails were all correct). That's because you pass this value to FB.api synchronously; i.e., you pass it within the loop, so it isn't closed over.
For a better understanding of closures in JavaScript, see How do JavaScript closures work?
To be clear: epascarello's answer should solve the problem for you. Wrapping up your logic in an anonymous function does introduce a new scope (a useful trick that can be used in a variety of scenarios), and so the value of albumName, etc. getting closed over is different for each callback. I just wanted to point out that it wasn't the i per se causing the problem (since your callback doesn't close over i at all).

Related

How to Fix: getJSON function repeats the same string while in a FOR Loop

I'm trying to assign src attribute for .poster class of a specified id, I am trying to get that from TMDB API.
movies array is assigned by PHP and it doesn't affect anything
Inside the for loop when I console.log the moviename, it is logged correctly But when I write the $.getJSON function and console.log the moviename inside the function, it logs the last item of the array("Superman+Red+Son")
How can I fix it, Code is belowThanks in advance.
movies = ["Star+Wars+Rise+Of+Skywalker","Lego+Movie","Lego+Movie+2+Second+Part","Superman+Red+Son"];
for (q of movies) {
moviename = q.replace(/\u002B/g, "");
console.log(moviename);
$.getJSON('https://api.themoviedb.org/3/search/movie?api_key=' + key + '&query=' + q + '', function (data) {
console.log(moviename);
poster = data.results[0].poster_path;
posterloc = 'https://image.tmdb.org/t/p/w342' + poster;
$('#' + moviename + ' .poster').attr('src', posterloc);
});
}
Probably this issue was caused by not defining variables using let,var or const

JQuery Mobile not executing my entire function

I'm trying to add a listview element based on my QR code. So when I scan one QR code, i take the first value of the array, and add a listview element with the text of the first value, and use a counter and so on. However, it doesn't increment my counter. All i get is that the function appendToList() gets terminated once listview refreshes. Count doesnt increment and ArrayIndex remains empty. How can I get the counter to work? And the array as well? The function doesn't execute all the way.
function scan() {
cordova.plugins.barcodeScanner.scan(
function(result) {
$("#nullExhibition").remove();
resetData(); // Removes the empty exhibition text
if (!result.cancelled) {
if (result.format == "QR_CODE") {
var value = result.text;
if(!localStorage.getItem("LocalData"))localStorage.setItem("LocalData","[]")
data = localStorage.getItem("LocalData");
//alert(data);
data = JSON.parse(data);
data.push(value);
localStorage.setItem("LocalData", JSON.stringify(data));
//location.reload();
alert(count);
appendToList();
// location.hash = '#Vote';
}
}
},
function(error) {
alert("Scanning failed: " + error);
}
);
}
//JJJ Function: creates table which will be refreshed in the homePage //CW Create the listview if not created
function appendToList() {
$("#list").append("<li id='" + count + "' style='font-family: 'Quicksand', sans-serif !important;' class='ui-content'><a href='javascript:pageGenerator()'>" + data[0] + "</a></li>");
$("#list").listview("refresh");
ArrayIndex.append(data[0]);
count ++;
}
function resetData(){
localStorage.removeItem("LocalData");
}
Where did you declare "count" in the global scope? You reference it in two separate functions, but are neither passing the value between them, nor made it a global var so that both functions reference the same variable. They way you have it, you reinitialize that var each time that you call the appendToList() function.
Add this to the top of your document (outside of the function closures):
var count = 0;

Twitch TV JSON API Issue

So,I am trying to use the twitch API:
https://codepen.io/sterg/pen/yJmzrN
If you check my codepen page you'll see that each time I refresh the page the status order changes and I can't figure out why is this happening.
Here is my javascript:
$(document).ready(function(){
var ur="";
var tw=["freecodecamp","nightblue3","imaqtpie","bunnyfufuu","mushisgosu","tsm_dyrus","esl_sc2"];
var j=0;
for(var i=0;i<tw.length;i++){
ur="https://api.twitch.tv/kraken/streams/"+tw[i];
$.getJSON(ur,function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> "+tw[j]+"<p>"+""+"</p></li>");
if(json.stream==null){
$(".stat").append("<li>"+"Offline"+"</li>");
}
else{
$(".stat").append("<li>"+json.stream.game+"</li>");
}
j++;
})
}
});
$.getJSON() works asynchronously. The JSON won't be returned until the results come back. The API can return in different orders than the requests were made, so you have to handle this.
One way to do this is use the promise API, along with $.when() to bundle up all requests as one big promise, which will succeed or fail as one whole block. This also ensures that the response data is returned to your code in the expected order.
Try this:
var channelIds = ['freecodecamp', 'nightblue3', 'imaqtpie', 'bunnyfufuu', 'mushisgosu', 'tsm_dyrus', 'esl_sc2'];
$(function () {
$.when.apply(
$,
$.map(channelIds, function (channelId) {
return $.getJSON(
'https://api.twitch.tv/kraken/streams/' + encodeURIComponent(channelId)
).then(function (res) {
return {
channelId: channelId,
stream: res.stream
}
});
})
).then(function () {
console.log(arguments);
var $playersBody = $('table.players tbody');
$.each(arguments, function (index, data) {
$playersBody.append(
$('<tr>').append([
$('<td>'),
$('<td>').append(
$('<a>')
.text(data.channelId)
.attr('href', 'https://www.twitch.tv/' + encodeURIComponent(data.channelId))
),
$('<td>').text(data.stream ? data.stream.game : 'Offline')
])
)
})
})
});
https://codepen.io/anon/pen/KrOxwo
Here, I'm using $.when.apply() to use $.when with an array, rather than list of parameters. Next, I'm using $.map() to convert the array of channel IDs into an array of promises for each ID. After that, I have a simple helper function with handles the normal response (res), pulls out the relevant stream data, while attaching the channelId for use later on. (Without this, we would have to go back to the original array to get the ID. You can do this, but in my opinion, that isn't the best practice. I'd much prefer to keep the data with the response so that later refactoring is less likely to break something. This is a matter of preference.)
Next, I have a .then() handler which takes all of the data and loops through them. This data is returned as arguments to the function, so I simply use $.each() to iterate over each argument rather than having to name them out.
I made some changes in how I'm handling the HTML as well. You'll note that I'm using $.text() and $.attr() to set the dynamic values. This ensures that your HTML is valid (as you're not really using HTML for the dynamic bit at all). Otherwise, someone might have the username of <script src="somethingEvil.js"></script> and it'd run on your page. This avoids that problem entirely.
It looks like you're appending the "Display Name" in the same order every time you refresh, by using the j counter variable.
However, you're appending the "Status" as each request returns. Since these HTTP requests are asynchronous, the order in which they are appended to the document will vary each time you reload the page.
If you want the statuses to remain in the same order (matching the order of the Display Names), you'll need to store the response data from each API call as they return, and order it yourself before appending it to the body.
At first, I changed the last else condition (the one that prints out the streamed game) as $(".stat").append("<li>"+jtw[j]+": "+json.stream.game+"</li>"); - it was identical in meaning to what you tried to achieve, yet produced the same error.
There's a discrepancy in the list you've created and the data you receive. They are not directly associated.
It is a preferred way to use $(".stat").append("<li>"+json.stream._links.self+": "+json.stream.game+"</li>");, you may even get the name of the user with regex or substr in the worst case.
As long as you don't run separate loops for uploading the columns "DisplayName" and "Status", you might even be able to separate them, in case you do not desire to write them into the same line, as my example does.
Whatever way you're choosing, in the end, the problem is that the "Status" column's order of uploading is not identical to the one you're doing in "Status Name".
This code will not preserve the order, but will preserve which array entry is being processed
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
if (json.stream == null) {
$(".stat").append("<li>" + "Offline" + "</li>");
} else {
$(".stat").append("<li>" + json.stream.game + "</li>");
}
})
}(i));
}
});
This code will preserve the order fully - the layout needs tweaking though
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
var name = $(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
var stat = $(".stat").append("<li></li>")[0].lastElementChild;
console.log(stat);
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
if (json.stream == null) {
$(stat).text("Offline");
} else {
$(stat).text(json.stream.game);
}
}).then(function(e) {
console.log(e);
}, function(e) {
console.error(e);
});
}(i));
}
});

Including objects from external .js files

I have been searching for many hours over several days for this answer and though there are many topics on how to include files in a project (also here at Stack Overflow), I have not yet found THE solution to my problem.
I'm working on a project where I want to include one single object at a time, from many different files (I do not want to include the files themselves, only their content). All the object in all the files have the same name, only the content is different.
It is important that I do not get a SCRIPT tag in the head section of the page as all the content from the files will have the same names. None of the files will have functions anyways, only one single object, that will need to be loaded one at the time and then discarded when the next element is loaded.
The objects will hold the data that will be shown on the page and they will be called from the menu by an 'onclick' event.
function setMenu() // The menu is being build.
{
var html = '';
html += '<table border="0">';
for (var i = 0; i<menu.pages.length; i++)
{
html += '<tr class="menuPunkt"><td width="5"></td><td onclick="pageName(this)">'+ menu.pages[i] +'</td><td width="5"></td></tr>';
}
// menu is a global object containing elements such as an array with
// all the pages that needs to be shown and styling for the menu.
html += '</table>';
document.getElementById("menu").innerHTML = html;
style.setMenu(); // The menu is being positioned and styled.
}
Now, when I click on a menu item the pageName function is triggered and I'm sending the HTML element to the function as well, it is here that I want the content from my external file to be loaded into a local variable and used to display content on the page.
The answer I want is "How to load the external obj into the function where I need it?" (It may be an external file, but only in the term of not being included in the head section of the project). I'm still loading the the file from my own local library.
function pageName(elm) // The element that I clicked is elm.
{
var page = info.innerHTML; // I need only the innerHTML from the element.
var file = 'sites/' + page + '.js'; // The file to be loaded is created.
var obj = ?? // Here I somehow want the object from the external file to be loaded.
// Before doing stuff the the obj.
style.content();
}
The content from the external file could look like this:
// The src for the external page: 'sites/page.js'
var obj = new Object()
{
obj.innerHTML = 'Text to be shown';
obj.style = 'Not important for problem at hand';
obj.otherStuff = ' --||-- ';
}
Any help will be appreciated,
Molle
Using the following function, you can download the external js file in an ajax way and execute the contents of the file. Please note, however, that the external file will be evaluated in the global scope, and the use of the eval is NOT recommended. The function was adopted from this question.
function strapJS(jsUrl) {
var jsReq = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
if (jsReq === null) {
console.log("Error: XMLHttpRequest could not be initiated.");
}
jsReq.onload = function () {
try {
eval(jsReq.responseText);
} catch (e) {
console.log("Error: The script file contains errors." + e);
}
};
try {
jsReq.open("GET", jsUrl, true);
jsReq.send(null);
} catch (e) {
console.log("Error: Cannot retrieving data." + e);
}
}
JSFiddle here
Edit: 1
After some refactoring, I came up with this:
function StrapJs(scriptStr, jsObjName) {
var self = this;
self.ScriptStr = scriptStr;
self.ReturnedVal = null;
function _init() {
eval(self.ScriptStr);
self.ReturnedVal = eval(jsObjName);
}
_init();
}
You can then get the script string any way you want and just instantiate a new StrapJs object with the script string and name of the object to return inside the script string. The ReturnedVal property of the StrapJs object will then contain the object you are after.
Example usage:
var extJS = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown', " +
" style : 'Not important for problem at hand', " +
" otherStuff : ' --||-- ' " +
"}; ";
var extJS2 = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown 2', " +
" style : 'Not important for problem at hand 2', " +
" otherStuff : ' --||-- 2' " +
"}; ";
var strapJS = new StrapJs(extJS, 'obj');
var strapJS2 = new StrapJs(extJS2, 'obj');
console.log(strapJS.ReturnedVal.innerHTML);
console.log(strapJS2.ReturnedVal.innerHTML);
See it in action on this fiddle

variable empty when doing an asynchronous call in node.js (closure)

Update: Solved, it was indeed a scope issue. I got around it by moving the user list code inside the database class and returning the prebuilt list.
Using node.js, I make an asynchronous call to function findUser and I build a list of users from the callback into the variable content. This works fine during the loop (where it says content variable is available) but when the loop exits, the variable is empty. How can I rewrite the code so that the value of variable content is available outside the loop?
exports.listUsers=function(req,res) {
var content=''
isLoggedIn=user.findUser({},function(myuser) {
content = content +'<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
})
//here value of content var is empty
console.log(content)
showPage(req,res,{'content':content})
}
If findUser() is asynchronous (which you indicate), then the issue is that findUser() has not yet completed when showPage() is called.
For asynchronous functions, you can only use their results from the success handler function. You can't call an asynchronous function and expect to use it synchronously like your current code.
I don't know exactly what you're trying to accomplish, but this is the general design pattern you would need to use:
exports.listUsers=function(req,res) {
isLoggedIn=user.findUser({},function(myuser) {
var content = '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
showPage(req,res,{'content':content})
});
}
Or, if the callback is being called many times once for each user, you can accumulate the content and call showPage() on the last callback:
exports.listUsers=function(req,res) {
var content = "";
isLoggedIn=user.findUser({},function(myuser) {
content += '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
// devise some logic to know when the last callback is being called
// perhaps based on a user count
if (this is the last user callback) {
showPage(req,res,{'content':content})
}
});
}

Categories