This question already has answers here:
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
(7 answers)
How do I return the response from an asynchronous call?
(41 answers)
Closed 8 years ago.
I can't figure out why specialArray is not keeping its values outside of $.getJSON. I thought I understood scope, any help appreciated. It spits out values to the console, but loses the values once it gets outside .getJSON. Any ideas?
var specialArray = [];
var data, temp, regionArrayNumber;
var numberOfRegions = 29;
var chartData = [];
$(document).ready(function() {
// set up array of objects, organized by region_id
for (var j = 0; j < numberOfRegions; j++) {
temp = {
"region_id" : (j + 1),
"number_of_reads" : 0,
"bus_count" : 0,
"reads_per_bus" : 0
};
chartData.push(temp);
}
$.getJSON('https://data.cityofchicago.org/resource/historical-traffic-congestion-region.json', function(data) {
// cycle through objects, add numbers to totals
for (var i = 0; i < data.length; i++) {
regionArrayNumber = data[i].region_id - 1; // get region id, offset because of zero-based array
chartData[regionArrayNumber].bus_count += parseInt(data[i].bus_count);
chartData[regionArrayNumber].number_of_reads += parseInt(data[i].number_of_reads);
}
// calculate avg reads per bus
for (var k = 0; k < chartData.length; k++) {
chartData[k].reads_per_bus = (parseInt(data[k].number_of_reads)) / (parseInt(data[k].bus_count));
}
// set up array for google chart
for (var x = 0; x < chartData.length; x++) {
var tempArray = [];
tempArray[0] = chartData[x].region_id;
tempArray[1] = parseInt(chartData[x].number_of_reads);
specialArray.push(tempArray);
console.log("Inside: " + specialArray[x][0] + ", " + specialArray[x][1]);
}
});
console.log("Outside: " + specialArray[1][0]);
}); // end of doc.ready
The good news is that you understand scope just fine :)
getJSON is an asynchronous function... that means it will kick off the service call, move on with the next statement (which is console.log("Outside: " + specialArray[1][0]);) and then when the service call completes it will get around to invoking your callback function (what you called 'inside').
If you want to act on the result of the service call, that code needs to either live inside the callback function or be invoked by something inside the callback function.
The reason getJSON and similar APIs are like this is because they want to make sure your code doesn't 'hang' and make the browser unresponsive while waiting for something that could take a while to complete. Kinda tricky to wrap your head around at first but it's doing you a favor.
Related
This question already has answers here:
JavaScript closure inside loops – simple practical example
(44 answers)
Closed 6 years ago.
I have a piece of code that I'm trying to have alert 1,2,3. I'm having issues using closures properly, so I can't figure this out.
The original code:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result.push( function() {alert(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// using j only to help prevent confusion - could use i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList();
I am trying to do something like this to buildList() to get it to work properly:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result[i] = function(x) {
result.push( function() {alert(item + ' ' + list[x])} );
}(i);
}
return result;
}
I know I'm making mistakes on working with the closures, I'm just not sure what the problem is.
Your second try was closer to the solution but still doesn't work because your inner-most function is capturing variable item from your top-level function: item is just always referencing the same instance, which was created when calling buildList().
var scope in JavaScript is always bound to current function call, not to code block, so it's not bound to control statements like for.
For that reason, the alerts likely show the value 'item' + (list.length-1) had at the time of calling buildList().
Since you are passing i to your closure, you should declare var item within that function, e.g:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
result[i] = function(x) {
// x and item are both local variables of anonymous function declared just above
var item = 'item' + list[x]; // or maybe you meant 'item' + x?
return function() {alert(item + ' ' + list[x])};
}(i);
}
return result;
}
Note that the closure would still capture a reference to list so will display the value it contains at the time of calling functions in the array returned by buildList(). Also local variable item is completely optional, you could call alert('item' + x /*or is it list[x]?*/ + ' ' + list[x]).
From How do JavaScript closures work?
Note that when you run the example, "item2 undefined" is alerted three
times! This is because just like previous examples, there is only one
closure for the local variables for buildList. When the anonymous
functions are called on the line fnlistj; they all use the same
single closure, and they use the current value for i and item within
that one closure (where i has a value of 3 because the loop had
completed, and item has a value of 'item2'). Note we are indexing from
0 hence item has a value of item2. And the i++ will increment i to the
value 3.
You need to make a closure in each loop iteration if you are to store the matching value of i:
function buildList(list) {
var result = [], item, closure;
for (var i = 0; i < list.length; i++) {
item = 'item' + list[i];
// call this function with the string you wish to store
// the inner function will keep a reference to the 'msg' parameter even after the parent function returns
closure = (function(msg) {
return function() {
alert(msg);
};
}(item + ' ' + list[i]));
result.push( closure );
}
return result;
}
function testList() {
var fnlist = buildList([1, 2, 3]);
// using j only to help prevent confusion - could use i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList();
Same question asked here and here. Same answers here, here, here, here and probably in dozen more places.
I'm only crawling in JS so probably the solution is obvious.
I'm writing a Chrome extension which in browser action (after clicking on extension's button) reads several pages and from each of them it retrieves an integer. Then, it creates a table with these integers. I'm doing it qith AJAX so it's asynchronous and I want to have all integers before creating the table.
I've read these topics:
Pass in an array of Deferreds to $.when()
How to tell when multiple functions have completed with jQuery deferred
...and wrote a code that doesn't work.
var sum = 0;
document.addEventListener('DOMContentLoaded', function () {
var deferreds = [];
var users = [...], pages = [...];
for(i = 0; i < users.length; ++i) {
for(j = 0; j < pages.length; ++j)
deferreds.push(window[pages[j]](users[i], pages[j]));
}
$.when.apply(null, deferreds).done(function() {
alert("sum: " + sum);
});
});
function Name(user, page) {
$.get("http://page2/" + user, function(data) {
sum += 7;
});
return 7;
}
function Name2(user, page) {
$.get("http://page/" + user, function(data) {
sum += 84;
});
return 84;
}
So the alert prints 0 instead of 91. I cut the part with table as all fields are "undefined" anyway. If I get sum = 91, I'll probably get the table.
As I said - obvious mistake. The functions should look like:
function Name(user, page) {
return $.get("http://page2/" + user, function(data) {
sum += 7;
});
}
Following code is given:
var a = [ ], i = 0, j = 0;
for (i = 0; i < 5; i += 1) {
(function(c) {
a.push(function () {
console.log(c); });
})(i);
};
for (j = 0; j < 5; j += 1) { a[j](); }
Why does i always get bigger by 1 instead of staying 5? Hasn't the foor loop already been passed, so the i parameter given to the anonymous function should be 5?
If you referenced i from the inner closure then yes, you would see the result being 5 in all cases. However, you pass i by value to the outer function, which is accepted as parameter c. The value of c is then fixed to whatever i was at the moment you created the inner closure.
Consider changing the log statement:
console.log("c:" + c + " i:" + i);
You should see c going from 0 to 4 (inclusive) and i being 5 in all cases.
chhowie's answer is absolutely right (and I upvoted it), but I wanted to show you one more thing to help understand it. Your inner function works similarly to a more explicit function call like this:
var a = [ ], i = 0, j = 0;
function pushFunc(array, c) {
array.push(function () {
console.log(c);
});
}
for (i = 0; i < 5; i += 1) {
pushFunc(array, i);
}
for (j = 0; j < 5; j += 1) { a[j](); }
Which should also help you understand how c comes from the function argument, not from the for loop. Your inner function is doing exactly the same thing as this, just without an externally declared named function.
This is the code without any attempt to add var nn = 99 to the loop
//note that the 'i' is a parameter of the function
function myFunction(arr, i) {
for (i = i ? i + 5 : 1; i < arr.length; i++) {
//...
}
}
When I try to add a new var it do things I don't want:
Edit: it seems this is wrong
for (var nn = 99, i = i ? i + 5 : 1; i < arr.length; i++)
//created a new 'i'
or
for (i = i ? i + 5 : 1, var nn = 99; i < arr.length; i++)
//doesn't work :(
I know it is exactly the same if I move it outside. But one of the things I hate most, is to not be able to understand what I meant when reading a old code after some months. Moving that line inside the loop will make me understand that line easier.
You can't modify the function's parameter. As i is a primitive value (number), JavaScript will call-by-value, not by-reference.
And as you name your second argument "i", it will be available as a local variable from start on. Using the var keyword with "i" somewhere won't change anything.
((i = i ? i + 5 : 1) * 0) + 99
This will always equal 99. But that does not matter, since it is still unclear what you are trying to accomplish.
If your goal is to loop through an array from a designated start spot then you can do this.
for (; i<arr.length; i++) {}
If you can't guarantee that i is a number, then you will have to perform some sort of checking.
for (var index=(i?i:0); index < arr.length; index++) {}
Javascript will pass primitives by value, so you cannot modify the source value from inside this function. You can return the modified value if want.
ivar = func(arr, ivar);
function func(arr, i){
for (;i<arr.length; i++) {}
return i;
}
I am really struggling with concept of scope in my code.
I am simply trying to create a 'callback' function which will add a className to a variable. As it's inside a function, I am passing the global variable as parameters to the callback function using the concept of closure (still dont understand how closure works).
var ePressCuttingsArray = $(".cPressCuttings");
var eSelectedPressCuttingsArray = [];
var iIndexArray = [];
for (var i = 0; i < 7; i++) {
var iIndexArrayValue;
// two conditions being checked in while loop, if random no. is not in global array (iIndexArray) & i var is equal to eSelectedPress... array
while (jQuery.inArray(((iIndexArrayValue = Math.floor(Math.random() * 14) + 1), iIndexArray) === -1)
&& (i === eSelectedPressCuttingsArray.length))
{
// to push a value at a position from array ePressCut... into eSelectedPress... array
eSelectedPressCuttingsArray.push(ePressCuttingsArray[iIndexArrayValue]);
// run a function to addClass to the recently pushed value in eSelectedPress... array
(function (i) {
$(eSelectedPressCuttingsArray[i]).addClass("cPressCuttingsDisplay0" + i)
} (i) );
iIndexArray.push(iIndexArrayValue);
}
}
Could someone explain why the closure func. is not performing correctly, i.e. it always successfully add the className "cPressCuttingsDisplay00", but doesnt follow that up with a className of "cPressCuttingsDisplay01" for the next loop iteration.
You should be able to accomplish your goal by using a for loop:
var ePressCuttingsArray = $(".cPressCuttings").makeArray();
var eSelectedPressCuttingsArray = [];
for (var i = 0; i < 7; i++) {
var idx = Math.floor(Math.random() * ePressCuttingsArray.length);
var selectedItem = ePressCuttingsArray[idx];
selectedItem.addClass('cPressCuttingsDisplay0' + i);
eSelectedPressCuttingsArray.push(selectedItem);
ePressCuttingsArray.splice(idx, 1);
}