javascript closures in loops - javascript

//I have the following function:
function handle_message(msg)
{
//do work
console.log('some work: '+msg.val);
//call next message
msg.next();
}
//And array of message objects:
var msgs = [ {val : 'first msg'}, { val : 'second msg'}, { val : 'third msg'}];
//I link messages by setting next parameter in a way that it calls handle_message for the next msg in the list. Last one displays alert message.
msgs[2].next = function() {alert('done!')};
msgs[1].next = function() {handle_message(msgs[2]);};
msgs[0].next = function() {handle_message(msgs[1]);};
//Start the message handle "chain". It works!
handle_message(msgs[0]);
//======== Now I do exactly the same thing but I link messages using the for loop:
for (var i=msgs.length-1; i>=0; i--)
{
if (i==msgs.length-1)
{
msgs[i].next = function() {alert('done!');};
}
else
{
msgs[i].next = function() {handle_message(msgs[i+1]);};
}
}
//Start the message handling chain. It fails! It goes into infinite recursion (second message calls itself)
handle_message(msgs[0]);
Can sombody explain why it happens? Or maybe an alternative to this pattern? My case is this: I receive an array with messages and I have to handle them in order, one ofter another SYNCHRONOUSLY. The problem is some of the messages require firing a series of animations (jqwuery animate() which is async) and the following messages cannot be handled until the last animation is finished. Since there is no sleep() in javascript I was trying to use such pattern where the message calls the next one after it is finished (in case of animations I simply pass the 'next' function pointer to animate's "complete" callback). Anyway, I wanted to build this 'chain' dynamically but discovered this strange (?) behaviour.

You need a closure to make it work:
function handle_message( msg ) {
console.log( 'some work: ' + msg.val );
msg.next();
}
var msgs = [{val :'first msg'},{val:'second msg'},{val:'third msg'}];
for ( var i = msgs.length - 1; i >= 0; i-- ) {
(function(i) {
if ( i == msgs.length - 1 ) {
msgs[i].next = function() { alert( 'done!' ); };
} else {
msgs[i].next = function() { handle_message( msgs[i + 1] ); };
}
})(i);
}
handle_message( msgs[0] );
Live demo: http://jsfiddle.net/simevidas/3CDdn/
Explanation:
The problem is with this function expression:
function() { handle_message( msgs[i + 1] ); }
This function has a live reference to the i variable. When this function is called, the for loop has long ended and the value of i is -1. If you want to capture the current value of i (the value during the iteration), you need to an additional wrapper function. This function captures the current value of i permanently (as an argument).

I think the problem is that i doesn't have the value you think it has:
// i is defined here:
for (var i=msgs.length-1; i>=0; i--)
{
if (i==msgs.length-1)
{
msgs[i].next = function() {alert('done!');};
}
else
{
msgs[i].next = function() {
// when this line gets executed, the outer loop is long finished
// thus i equals -1
handle_message(msgs[i+1]);
};
}
}
See point #5 Closures in loops at http://blog.tuenti.com/dev/top-13-javascript-mistakes/

Think about the values you are capturing in the closure.
msgs[i].next = function() {handle_message(msgs[i+1]);};
This captures the value of i, but it changes the next iteration so you get an infinite loop.
By the end of the loop i is -1 so i+1 is going just going to be the same message over and over again.

Related

Why doesn't my object's property update in JavaScript?

I have a function, which I have prepared for a constructor call...
function Queue() {
if (!(this instanceof Queue)) return new Queue();
this.capacity = {};
this._count = 0;
}
And these method are being set on the the prototype property of Queue...Everything kosher right?
Queue.prototype.enqueue = function(name, options) {
this.capacity[name] = options || {};
this.count();
if (this._count > 5) {
return 'Max capacity has been reached—which is five, please dequeue....'
}
};
Queue.prototype.count = function() {
var total = Object.keys(this.capacity);
total.forEach(function(elem) {
this._count++
});
if (this._count == 1) {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There is one item in the queue');
} else {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There are ' + this._count + ' items in the queue');
}
};
My question how do i get this._count to increment when the enqueue/count method fires? I keep getting:
There are 0 items in the queue
I know I could add it on the .prototype property and place that in the count function and have it reference a local var...
Queue.prototype.count = function() {
var total = Object.keys(this.capacity), count = 0;
total.forEach(function(elem) {
this.count++
});
Queue.prototype.call = call // <-- weird no?
if (this.count == 1) {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There is one item in the queue');
} else {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There are ' + this.count + ' items in the queue');
}
};
But that seems not to be elegant...
Thanks in advance!
You need to bind this within forEach
Queue.prototype.count = function() {
var total = Object.keys(this.capacity);
total.forEach(function(elem) {
this._count++
}.bind(this)); //bind the context
if (this._count == 1) {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There is one item in the queue');
} else {
console.log(this.capacity[Object.keys(this.capacity)])
console.log( 'There are ' + this._count + ' items in the queue');
}
};
Try following modification (bind the function):
total.forEach(function(elem) {
this._count++
}.bind(this));
The problems is that this refers to a different object than in the parent function, because in JS, closures do not preserve this but instead the caller decides the this value. Alternatively, you can use the second thisArg argument of foreach.
The existing answers provide good solutions to the problem itself, I just thought I'd elaborate a bit more on the why.
this is a reference assigned by the execution context. More plainly it's a reference that's determined by the call site of the function. Since you can pass functions around in JavaScript like any other value this can lead to problems being caused by that reference being a moving target.
The issue with your code is that you're referring to this inside of a forEach. forEach takes a function as an argument and calls it, since what this is pointing to is determined by where the function is called and not where it's defined the value is something different when it gets called. It ends up falling back to whatever global context or undefined if you're in strict mode.
There are a number of different ways to handle the problem.
You could store a reference to the outer this on a variable and use it inside the other function.
var self = this;
total.forEach(function(elem) {
self._count++;
});
You could use .bind. It's a function method which returns a function that uses the passed in object as the reference for this no matter where you call it.
total.forEach(function(elem) {
this._count++;
}.bind(this));
Or you could use an arrow function. Arrow functions don't create their own context so they'll just maintain the value of this from the surrounding one.
total.forEach(() => {
this._count++;
});
This is a common problem and these are all valid solutions. They go from least to most elegant in my opinion.

Exit the loop abruptly

Let's imagine the following code:
function DoSomethingHard(parameter1, parameter2, parameter3){
// Do Something Hard Here
}
var i;
for(i = 0; i <= stuff.length; i++) {
// "stuff" is an array
DoSomethingHard(stuff[i].something1, stuff[i].something2, stuff[i].something3);
}
$( "#button_to_cancel" ).click(function() {
//something to cancel
});
Suppose the array "stuff" has 100 positions, so the for loop will run
100 times, ie, it will do "Do Something Hard" 100 times.
Let's also consider that "DoSomethingHard" takes about 5 seconds to run
completely.
My question is: How do I manage the cancellation of "DoSomethingHard"? For example, if it has already run 50 times, how can I cancel the subsequent executions through a button? I did not succeed in my attempts and it always ends up running the whole loop ....
Thanks in advance :)
Javascript is single threaded, and a for loop executes until it is finished. I would do something like this to allow time for the cancel.
function DoSomethingHard(param){
//do something
}
var i = 0;
var loopInterval = setInterval(function() {
if (i >= stuff.length) {
clearInterval(loopInterval);
return;
}
DoSomethingHard(stuff[i]);
i++
}, 10);
$( "#button_to_cancel" ).click(function() {
clearInterval(loopInterval);
});
You can make use of setInterval to call the function and when you have a click event you can clear the intervals
var mytimeout;
var i;
for(i = 0; i <= stuff.length; i++) {
// "stuff" is an array
mytimeout = window.setInterval(DoSomethingHard(stuff[i].something1, stuff[i].something2, stuff[i].something3), 2000);
}
$( "#button_to_cancel" ).click(function() {
//something to cancel
window.clearInterval(mytimeout)
});
Simplest way as I see it:
function DoSomethingHard(parameter1, parameter2, parameter3){
//Do Something Hard Here
}
var i;
var active = true; //as of now, we want to process stuff
for(i=0;i<=stuff.length;i++){
//"stuff" is an array
if(active){
DoSomethingHard(stuff[i].something1, stuff[i].something2, stuff[i].something3);
}else {
active = true; //reset active in case we want to run loop again later on
break; // break out of loop
}
}
$( "#button_to_cancel" ).click(function() {
active = false;
});
You can't easily cancel it with a click on a button, unless you use recursion or iterators instead of a loop.
But you can cancel the loop inside itsself with a break; statement when some condition is met. For example you could write:
var result;
for(i=0;i<=stuff.length;i++){
result = DoSomethingHard(stuff[i].something1, stuff[i].something2, stuff[i].something3);
if (result === 'error' || i === 50) break;
}
This will end the loop if result becomes the 'error' (or anything else your return from inside the function) or when i reaches 50.
Now that i think of it, it's possible with a button click, but it requires more code and is inefficient. give me a minute.
Update: I would not advice this ppttern either, but it's pretty flexible:
var exitCondition,
doSomethingHard = function doSomethingHard(parameter1, parameter2, parameter3){
// Do Something Hard Here
},
i,
length = stuff.length,
result;
for (i = 0; i <= length; i++) {
// "stuff" is an array
result = doSomethingHard(stuff[i].something1, stuff[i].something2, stuff[i].something3);
if (exitCondition( result, i )) break;
}
$( "#button1_to_add" ).click(function() {
exitCondition = function( result, index ) {
return index === 50;
});
});
$( "#button2_to_cancel" ).click(function() {
exitCondition = null;
});
The clue here is to have an exit condition (or multiple) that you check inside the loop and have the button update this condition.
You can not stop a for loop from UI interaction because everything is running in a single thread and your action will execute only after loop executes completely. You can use setInterval as #jason p said.
I solve this way:
function DoSomethingHard(parameter1, parameter2, parameter3){
//Do Something Hard
var timer = window.setInterval(function(){
var page = $$('.page').data('page');
console.log("The page is: "+page);
if(page != 'preview'){
//Cancel...
console.log('Aborted');
clearInterval(timer);
}
},1000);
}
That is, i changed the scope. Instead using button click, i monitered when user leave the page, so cancel it.
You need to note that loop is synchronous where as your function isn't. Loop won't wait for DoSomethingHard() to compleye before the next iteration begins.In just a few milliseconds DoSomethingHard has been called over a hundred times! and your loop gets over so in essence break is of no use here.I think no language construct can help here
So what to do?
You need to decide whether to do something or not in the function itself create a global flag and check it inside the function
function DoSomethingHard(){
if(flag50done){
return;
}else{
//do what this fn was meant for
}
}
You can change value of flag50done with a click of a button and further actions would get stopped due to the return
In case DoSomethingHard is some 3rd party function which you cannot modify you can wrap it in another function say runDecider
function runDecider(a,b,c){
//add flag check here
if(flag50done){
return;
}else{
DoSomethingHard(a, b, c);
}
}
and call this in the loop
var result;
for(i=0;i<=stuff.length;i++){
result = runDecider(stuff[i].something1, stuff[i].something2, stuff[i].something3);
}

For loop does not allow setTimeout to execute

I have the below function that logs a character in a sentence passed to a function at a certain given time, have a look at the function below:
function print_string(param , timer) {
var strt_point = 0,
str_len = param.length,
self = this;
//typewritter();
typeit();
function typeit() {
setTimeout(function(){
console.log(param.substring(strt_point).charAt(0));
if(strt_point < str_len ) {
strt_point++;
typeit()
}
} , timer);
}
}
var str = print_string("hey there , whats up , hows the weather in Manhatten" , 50);
console.log(str);
The above programmer works fine, now if i add a for loop to the above programme I.E. WRAP THE setTimeout in a for loop ,
function print_string(param , timer) {
var strt_point = 0,
str_len = param.length,
self = this;
for(var i = 0 ; i < str_len ; i++) {
//typewritter();
typeit();
function typeit() {
setTimeout(function(){
console.log(param.substring(strt_point).charAt(0));
if(strt_point < str_len ) {
strt_point++;
typeit()
}
} , timer);
}
}
var str = print_string("hey there , whats up , hows the weather in Manhatten" , 50);
console.log(str);
All the characters are printed at once , Why ?
Why is it that the for loop does not honor the setTimeout interval ? can anybody explain ?
If you want the timer argument to act as an actual interval, use this:
function print_string(param, timer) {
for(var i = 0 ; i < param.length ; i++) {
setTimeout((function(char){
return function () {
console.log(char);
}
})(param.charAt(i)), timer * i);
}
}
var str = print_string("hey there , whats up , hows the weather in Manhatten" , 500);
Here is a fiddle.
The confusion for you is that a for loop happens immediately, or as fast as the processor will allow it. When the setTimeout handler executes, it has a different scope to what you're probably expecting. The scope is no longer within the for loop (because that happened in a different tick of the processor) so the print variable is lost. In order to get around this, I'm using what is called a closure. I'm building a function on the fly, to print the specific variable that I need, and passing it as an argument to setTimeout.
Read more about closures here.
The difference of the two pieces of code is:
In the first one you set the timer each time the timeout function is triggered.
In the second case you set all the timers simultaneously (almost) in each "foreach" iteration, so the events fire the same time.
You can prevent it by timer+=50.
I'll try to exlain:
You are passing a string with 30 characters to the function print_string so that the for loop will iterate and call setTimeout 30 times. After 50ms the first setTimeout callback will be called and will output the first char of your string. The variable strt_point will be incremented. Then the second setTimeout callback of your second for loop iteration will be called immediately and because strt_point is has already been incremented, the second char will be printed.
The problem is that you have ONE variable strt_point for all iterations of the for loop so that all chars are printed after 50ms.
I think you want something like this:
var print_string = function ( param, interval ) {
var
i = 0,
len = param.length,
result = document.getElementById( 'result' ),
// function called for every characters
next = function () {
// get the current character
var char = param.charAt(i);
// do what you want with them
result.innerHTML += char;
// if not the end of the string
if ( ++i < len ) {
// continue after the interval
setTimeout( next, interval );
}
}
next();
}
print_string( 'hey there , whats up , hows the weather in Manhatten!', 50 );
<div id="result"></div>

Getting the right index of an object in a loop in javascript

I have a list of nodes and I am going to draw each node using a raphael object. I have the following loop:
for(var i=0; i<curNodes.length; i++){
var node = curNodes[i];
var obj = paper.rect(node.getX(), node.getY(), node.width, node.getHeight())
.attr({fill:nodes[i].getColor(), "fill-opacity": 0.6})
.click(function(e){ onMouseClicked(i,e); });
}
on click, I want to call a function which can view some data associated with 'i' th element of curNodes array. However, all the time the last 'i' is passed to this function. my function is:
var onMouseClicked = function(i, event){
switch (event.which) {
case 1:
this.attr({title: curNodes[i].name});
break;
}
}
How should I access the correct index when calling a function?
Try this:
.click((function (i) {
return function (e) {
onMouseClicked(i,e);
};
})(i));
Like you noticed, the value of i (or the parameter in your function) is the last index from the for loop. This is because the click event doesn't happen immediately - the binding does, but the value of i is not captured. By the time the click handler actually is executed (when the click event is triggered in whatever way), the for loop has completed (a long time ago), and the value of i is the final value of the loop. You can capture the value of i with a closure like the above.
Although I know another way of handling, cleaner in my opinion, is to use:
.click(clickHandler(i));
function clickHandler(index) {
return function (e) {
onMouseClicked(i, e);
};
}
It is because of the closure variable i.
for (var i = 0; i < curNodes.length; i++) {
(function(i) {
var node = curNodes[i];
var obj = paper.rect(node.getX(), node.getY(), node.width,
node.getHeight()).attr({
fill : nodes[i].getColor(),
"fill-opacity" : 0.6
}).click(function(e) {
onMouseClicked(i, e);
});
})(i);
}

Do not continue Javascript for-loop until specified

I need to pause a for loop and not continue until I specify. For each item in the array that I'm looping through, I run some code that runs an operation on a separate device, and I need to wait until that operation is finished before looping to the next item in the array.
Fortunately, that code/operation is a cursor and features an after: section.
However, it's been running the entire for loop instantly, which I need to prevent. Is there any way to prevent the loop from continuing until specified? Or perhaps a different type of loop or something that I should use?
My first (poor) idea was to make a while-loop within the for-loop that ran continuously, until the after: portion of the cursor set a boolean to true. This just locked up the browser :( As I feared it would.
Anything I can do? I'm fairly new to javascript. I've been enjoying my current project though.
Here's the while-loop attempt. I know it's running the entire loop immediately because the dataCounter goes from 1 to 3 (two items in the array currently) instantly:
if(years.length>0){
var dataCounter = 1;
var continueLoop;
for(var i=0;i<years.length;i++){
continueLoop = false;
baja.Ord.make(historyName+"?period=timeRange;start="+years[i][1].encodeToString()+";end="+years[i][2].encodeToString()+"|bql:select timestamp, sum|bql:historyFunc:HistoryRollup.rollup(history:RollupInterval 'hourly')").get(
{
ok: function (result) {
// Iterate through all of the Columns
baja.iterate(result.getColumns(), function (c) {
baja.outln("Column display name: " + c.getDisplayName());
});
},
cursor: {
before: function () {
baja.outln("Called just before iterating through the Cursor");
counter=0;
data[dataCounter] = [];
baja.outln("just made data["+dataCounter+"]");
},
after: function () {
baja.outln("Called just after iterating through the Cursor");
continueLoop = true;
},
each: function () {
if(counter>=data[0].length) {
var dateA = data[dataCounter][counter-1][0];
dateA += 3600000;
}
else {
var dateA = data[0][counter][0];
}
var value=this.get("sum").encodeToString();
var valueNumber=Number(value);
data[dataCounter][counter] = [dateA,valueNumber];
counter++;
},
limit: 744, // Specify optional limit on the number of records (defaults to 10)2147483647
offset: 0 // Specify optional record offset (defaults to 0)
}
})
while(continueLoop = false){
var test = 1;
baja.outln("halp");
}
dataCounter++;
}
}
Do not use a for loop to loop on each element. You need, in the after: to remember which element of the array you've just done and then move to the next one.
Something like this :
var myArray = [1, 2, 3, 4]
function handleElem(index) {
module.sendCommand({
..., // whatever the options are for your module
after: function() {
if(index+1 == myArray.length) {
return false; // no more elem in the array
} else {
handleElem(index+1)} // the after section
}
});
}
handleElem(0);
I assumed that you call a function with some options (like you would for $.ajax()) and that the after() section is a function called at the end of your process (like success() for $.ajax())
If the "module" you call is not properly ended in the after() callback you could use setTimeout() to launch the process on the next element with a delay
EDIT: With your real code it would be something like this :
function handleElem(index) {
baja.Ord.make("...start="+years[index][1].encodeToString()+ "...").get(
{
ok: ...
after: function() {
if(index+1 == years.length) {
return false; // no more elem in the array
} else {
handleElem(index+1)} // the after section
}
}
});
}

Categories