javascript variable scope/closure in loop after ti

2019-01-29 09:38发布

问题:

It's late and the part of my brain where Douglas Crockford lives is closed. Ive tried a few things but nothing's doing as expected.

I've got a canvas where I draw a 2 lines, then fade them out on a timer but only the last line in the loop is being faded out. Here's my fiddle, look down to line 50ish in the JS, to see it in action drag your mouse around in the bottom right pane:

http://jsfiddle.net/mRsvc/4/

this is the function, basically the timeout only gets the last value in the loop, I've seen this before and I'm sure if I wasn't so delirious it might be simpler. Here's the function in particular:

function update()
        {
            var i;
            this.context.lineWidth = BRUSH_SIZE;            
            this.context.strokeStyle = "rgba(" + COLOR[0] + ", " + COLOR[1] + ", " + COLOR[2] + ", " +  BRUSH_PRESSURE + ")";
            for (i = 0; i < scope.painters.length; i++)
            {
                scope.context.beginPath();
                var dx = scope.painters[i].dx;
                var dy = scope.painters[i].dy;
                scope.context.moveTo(dx, dy);   
                var dx1 = scope.painters[i].ax = (scope.painters[i].ax + (scope.painters[i].dx - scope.mouseX) * scope.painters[i].div) * scope.painters[i].ease;
                scope.painters[i].dx -= dx1;
                var dx2 = scope.painters[i].dx;
                var dy1 = scope.painters[i].ay = (scope.painters[i].ay + (scope.painters[i].dy - scope.mouseY) * scope.painters[i].div) * scope.painters[i].ease;
                scope.painters[i].dy -= dy1;
                var dy2 = scope.painters[i].dy;
                scope.context.lineTo(dx2, dy2);
                scope.context.stroke();
                for(j=FADESTEPS;j>0;j--)
                {
                    setTimeout(function()
                        {
                            var x=dx,y=dy,x2=dx2,y2=dy2;
                            scope.context.beginPath();
                            scope.context.lineWidth=BRUSH_SIZE+1;
                            scope.context.moveTo(x, y);
                            scope.context.strokeStyle = "rgba(" + 255 + ", " + 255 + ", " + 255 + ", " + .3 + ")";
                            scope.context.lineTo(x2, y2);
                            scope.context.stroke();
                            scope.context.lineWidth=BRUSH_SIZE;
                        },
                    DURATION/j);
                }
            }
        }

回答1:

The problem is that the variables dx, dy, etc that you refer to in the function you pass to setTimeout() are defined in the surrounding scope and by the time any of the timeouts actually runs these variables all hold the values from the last iteration of the loop(s).

You need to create an extra containing function to close over the values from each iteration. Try something like the following:

for(j=FADESTEPS;j>0;j--) {
   (function(x,y,x2,y2) {
      setTimeout(function() {
         scope.context.beginPath();
         scope.context.lineWidth=BRUSH_SIZE+1;
         scope.context.moveTo(x, y);
         scope.context.strokeStyle = "rgba(" + 255 + ", " + 255 + ", " + 255 + ", " + .3 + ")";
         scope.context.lineTo(x2, y2);
         scope.context.stroke();
         scope.context.lineWidth=BRUSH_SIZE;
      },
      DURATION/j);
   })(dx, dy, dx2, dy2);
}

This creates a new anonymous function for each iteration of the j=FADESTEPS loop, executing it immediately and passing the dx, etc. values as they were at the time each iteration of the loop ran, and moving the x, y, etc. variables out of your existing function and making them parameters of the new one so then by the time the timeout runs it will use the correct values.



回答2:

You can try something like this:

`<script>
for(j=10;j>0;j--)
                {
                var fn = function(ind){return function()
                        {
                            console.log(ind);
                        };
                        }(j);
                    setTimeout(fn,
                    1000);
                }
</script>`


回答3:

Or another way (as soon as you do not use IE, but let it learn canvas at first :))

for(j=FADESTEPS;j>0;j--)
{
   setTimeout(function(x,y,x2,y2)
     {
        scope.context.beginPath();
        scope.context.lineWidth=BRUSH_SIZE+1;
        scope.context.moveTo(x, y);
        scope.context.strokeStyle = "rgba(" + 255 + ", " + 255 + ", " + 255 + ", " + .3 + ")";
        scope.context.lineTo(x2, y2);
        scope.context.stroke();
        scope.context.lineWidth=BRUSH_SIZE;
     },
     DURATION/j,dx,dy,dx2,dy2);
}

ps: there is no need in set of extra functions (the reasons are clear)



回答4:

  1. First of all j is a global.
  2. Second of all, you never close the paths that you begin, which can cause memory leaks. It seems really slow and this may be why. You need to call closePath() whenever you're done with the paths you start with beginPath()
  3. Next, I think there's some general funniness with how this works. You're fading out by drawing over the last thing with white. I've done something similar to this before, but instead I cleared the whole screen and kept drawing things over and over again. It worked okay for me.

Explanation

The other answers about dx and dy being passed from the higher scope are the right answers though. Async functions defined in synchronous for loops will take the last version of the state.

for (var i = 0; i < 10; i++) setTimeout(function() { console.log(i)}, 10 )
10
10
// ...


回答5:

I would suggest you to use an array and store the points avoiding setTimeOut call in a loop. Somewhat like this.

    this.interval = setInterval(update, REFRESH_RATE);

    var _points = [];

    function update() {
        var i;
        this.context.lineWidth = BRUSH_SIZE;
        this.context.strokeStyle = "rgba(" + COLOR[0] + ", " + COLOR[1] + ", " + COLOR[2] + ", " + BRUSH_PRESSURE + ")";
        for (i = 0; i < scope.painters.length; i++) {
            scope.context.beginPath();
            var dx = scope.painters[i].dx;
            var dy = scope.painters[i].dy;
            scope.context.moveTo(dx, dy);
            var dx1 = scope.painters[i].ax = (scope.painters[i].ax + (scope.painters[i].dx - scope.mouseX) * scope.painters[i].div) * scope.painters[i].ease;
            scope.painters[i].dx -= dx1;
            var dx2 = scope.painters[i].dx;
            var dy1 = scope.painters[i].ay = (scope.painters[i].ay + (scope.painters[i].dy - scope.mouseY) * scope.painters[i].div) * scope.painters[i].ease;
            scope.painters[i].dy -= dy1;
            var dy2 = scope.painters[i].dy;
            scope.context.lineTo(dx2, dy2);
            scope.context.stroke();
            _points.push([dx, dy, dx2, dy2]);

            clear();
        }
    }

    function clear(){

        if(_points.length < FADESTEPS){
            return;
        }

        var p = _points.shift();
                    if(!p){
                        return;
                    }
                    var x = p[0],
                        y = p[1],
                        x2 = p[2],
                        y2 = p[3];
                    scope.context.beginPath();
                    scope.context.lineWidth = BRUSH_SIZE + 1;
                    scope.context.moveTo(x, y);
                    scope.context.strokeStyle = "rgba(" + 255 + ", " + 255 + ", " + 255 + ", " + .3 + ")";
                    scope.context.lineTo(x2, y2);
                    scope.context.stroke();
                    scope.context.lineWidth = BRUSH_SIZE;

    }

I know this is not exactly what you need, but I think this can be modified to get it.