4

I'm trying to execute a setTimeout() function inside of a for loop in Javascript, but I am getting the error "shape is undefined", even though it is defined, and I'm passing that as a parameter in the function within the setTimeout() call. The function works just fine if I delete the setTimeout enclosure.

Why am I getting this error and how can I fix it?

Thanks!

function fadeShapes(layer, movement, opacity, speed) {
    var shapes = layer.getChildren();

    for(var n = 0; n < shapes.length; n++) {
        var shape = layer.getChildren()[n];
        setTimeout(function(shape){
            shape.transitionTo({
                alpha: opacity,
                duration: speed
            }); 
        }, 100);                
    }
}
4
  • 1
    Note that all of the setTimeout calls will be set to run at the same time after a 100ms delay (give or take a ms or two), they will not run 100ms apart Commented Aug 6, 2012 at 0:16
  • you read my mind, Rob! How would I achieve the functionality you're describing? Commented Aug 6, 2012 at 0:24
  • @j-man86 I've added an answer detailing a solution. Commented Aug 6, 2012 at 0:30
  • @j-man86—you can set them all in the loop with an incrementing counter as suggested by Greg, or you can have each one call the next one until some limit is reached. Commented Aug 6, 2012 at 2:35

5 Answers 5

12

JavaScript does not have block scope so all of your timeout functions are pointing at the same variable shape, which after the loop finishes points to an undefined index of your array. You can use an anonymous function to emulate the scope you're looking for:

for(var n = 0; n < shapes.length; n++) {
    var shape = shapes[n]; //Use shapes so you aren't invoking the function over and over
    setTimeout((function(s){
        return function() { //rename shape to s in the new scope.
            s.transitionTo({
                alpha: opacity,
                duration: speed
            });
        };
    })(shape), 100);   
}

As you could tell by my issues getting the brackets matched up, this can be a little tricky. This can be cleaned up with ES5's Array.forEach:

layer.getChildren().forEach(function(shape) { //each element of the array gets passed individually to the function
    setTimeout(function(shape){
        shape.transitionTo({
            alpha: opacity,
            duration: speed
        }); 
    }, 100);                
});

forEach is built into modern browsers but can be shimmed in Internet Explorer older browsers.

Sign up to request clarification or add additional context in comments.

7 Comments

+1 for the use of 'block scope'. var shape actually gets 'hoisted' to the beginning of the function fadeShapes, which is it's scope.
In the original question it doesn't matter what shape is pointing to after the loop finishes since when the timeout function fires shape is out of scope entirely.
@kojiro: That's not how scope works in JavaScript. A function never loses its original scope. JavaScript functions are closures, which means they carry their original scope with them irrespective of where they're invoked.
@amnotiam yes, and the shape with a value is not the same shape as the parameter for the function passed to setTimeout.
@kojiro: Right. That parameter is undefined. It would need to have the value passed to it, or be removed, which would leave us with the shape in the fadeShapes function scope.
|
1

This is a common closure problem, here is the fixed code:

function fadeShapes(layer, movement, opacity, speed) {
    var shapes = layer.getChildren();

    for(var n = 0; n < shapes.length; n++) {
        var shape = layer.getChildren()[n];
        setTimeout((function (bound_shape) {
            return function() {  // return function!
                bound_shape.transitionTo({
                    alpha: opacity,
                    duration: speed
                }); 
            };
        }(shape)) // immediate execution and binding
        , 100);                
    }
}

What happens in your code is that the for loop will run and n functions will be scheduled from execution in 100ms, but the value of shape changes! So when your callback gets called shape is the value of shapes[length-1] (the last shape).

To fix it, you must 'close' the value using a closure. In this case a function that binds on the value of shape and returns the function you want executed in 100ms.

2 Comments

The Correct one. for(var n = 0; n < shapes.length; n++) { var shape = layer.getChildren()[n]; setTimeout((function (bound_shape) { return function() { // return function! bound_shape.transitionTo({ alpha: opacity, duration: speed }); }; }(shape)) // immediate execution and binding , 100); }
I guess you mean that bound_shape should be move to the outer function?
0

Here is the code for an iterator that respects the 100ms delay. (untested)

function iterate(array, timeout, callback) {
    var n = 0, length = array.length;
    function step() {
        callback(array[n]);
        n += 1;
        if (n < length) {   // are there more elements?
            setTimeout(step, timeout);
        }
    }
    setTimeout(step, timeout);  // start
}

function fadeShapes(layer, movement /* unused> */, opacity, speed) {
    var shapes = layer.getChildren();
    iterate(shapes, 100, function (shape) {
        shape.transitionTo({
            alpha: opacity,
            duration: speed
        }); 
    });
}

Note that by using the iterate function in this way the closure issues are solved as well.

2 Comments

I used a setInterval() function... which do you think is more efficient?
There isn't much different between setInterval and setTimeout, performance wise. setTimeout may give problems if you iterate over a large number of elements because of the limit on the callstack, setInterval does not have this problem. Just make sure you clear the timer (clearInterval) once you're done or you'll be eating up resources.
0

If you want the the setTimeout calls to be executed 100ms apart, then you would just add 100ms to each set Timeout call:

function fadeShapes(layer, movement, opacity, speed) {
    var shapes = layer.getChildren();

    for(var n = 0; n < shapes.length; n++) {
        var shape = shapes[n];
        setTimeout((function(local_shape){
            return function(){
                local_shape.transitionTo({
                    alpha: opacity,
                    duration: speed
                });
             } 
        })(shape), 100 + n*100);                
    }
}

Comments

0

Try:

function fadeShapes(layer, movement, opacity, speed) {
    var shapes = layer.getChildren();

    for(var n = 0; n < shapes.length; n++) {
        var shape = layer.getChildren()[n];
        (function(sh) {
            setTimeout(function(){
                sh.transitionTo({
                    alpha: opacity,
                    duration: speed
                }); 
            }, 100); 
        })(shape);
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.