最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Why do I lose my instance information when calling a prototype function via setTimeout? - Stack Overflow

programmeradmin7浏览0评论

I think I'm missing some key concept regarding objects and prototype functions in JavaScript.

I have the following:

function Bouncer(ctx, numBalls) {
    this.ctx = ctx;
    this.numBalls = numBalls;
    this.balls = undefined;
}

Bouncer.prototype.init = function() {
    var randBalls = [];
    for(var i = 0; i < this.numBalls; i++) {
        var x = Math.floor(Math.random()*400+1);
        var y = Math.floor(Math.random()*400+1);
        var r = Math.floor(Math.random()*10+5);
        randBalls.push(new Ball(x, y, 15, "#FF0000"));
    }
    this.balls = randBalls;
    this.step();
}

Bouncer.prototype.render = function() { 
    this.ctx.clearRect(0, 0, 400, 400);  
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].render(this.ctx);
    }
}

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    setTimeout(this.step, 1000);
}

I then create an instance of Bouncer and call its init function like so:

$(function() {
    var ctx = $('#canvas')[0].getContext('2d');
    var width = $('#canvas').width();
    var height = $('#canvas').height();



    var bouncer = new Bouncer(ctx, 30);
    bouncer.init();
});

The init() function will call step which has a setTimeout to loop the step function.

This works on the first call to step(). However, on the second call (when setTimeout fires step) the instance variable "balls" is undefined. So, in my step function, the second call will blow up saying there is no "length" property for undefined.

Why do I lose my instance information when calling step from setTimeout()?

How could I restructure this so I can loop via a timeout and still have access to those instance variables?

I think I'm missing some key concept regarding objects and prototype functions in JavaScript.

I have the following:

function Bouncer(ctx, numBalls) {
    this.ctx = ctx;
    this.numBalls = numBalls;
    this.balls = undefined;
}

Bouncer.prototype.init = function() {
    var randBalls = [];
    for(var i = 0; i < this.numBalls; i++) {
        var x = Math.floor(Math.random()*400+1);
        var y = Math.floor(Math.random()*400+1);
        var r = Math.floor(Math.random()*10+5);
        randBalls.push(new Ball(x, y, 15, "#FF0000"));
    }
    this.balls = randBalls;
    this.step();
}

Bouncer.prototype.render = function() { 
    this.ctx.clearRect(0, 0, 400, 400);  
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].render(this.ctx);
    }
}

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    setTimeout(this.step, 1000);
}

I then create an instance of Bouncer and call its init function like so:

$(function() {
    var ctx = $('#canvas')[0].getContext('2d');
    var width = $('#canvas').width();
    var height = $('#canvas').height();



    var bouncer = new Bouncer(ctx, 30);
    bouncer.init();
});

The init() function will call step which has a setTimeout to loop the step function.

This works on the first call to step(). However, on the second call (when setTimeout fires step) the instance variable "balls" is undefined. So, in my step function, the second call will blow up saying there is no "length" property for undefined.

Why do I lose my instance information when calling step from setTimeout()?

How could I restructure this so I can loop via a timeout and still have access to those instance variables?

Share Improve this question asked Sep 6, 2011 at 15:40 EdwardEdward 333 bronze badges
Add a ment  | 

6 Answers 6

Reset to default 6

When you call setTimeout(this.step, 1000);, the step method loses its desired context of this, as you're passing a reference to the step method. In the way that you're doing it now, when this.step gets called through setTimeout, this === window rather than your Bouncer instance.

This is easy to fix; just use an anonymous function, and keep a reference to this:

Bouncer.prototype.step = function() {
    var that = this; // keep a reference
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    setTimeout(function () { 
        that.step()
    }, 1000);
}

When you call a Javascript function, the value of this is determined by the call site.

When you pass this.step to setTimeout, the this is not magically preserved; it just passes the step function itself.
setTimeout calls its callback with this as window.

You need to create a closure that calls step on the right object:

var me = this;
setTimeout(function() { me.step(); }, 500);

For more information on the difference between this and closures, see my blog.

This is fairly standard 'this' scope issues. Many, many questions on SO regarding mis-understanding the context of 'this' when executing functions. I remend you read-up on it.

However, to answer your question, it works because you are calling this.step(), and 'this', in that context, is your desired Bouncer instance.

The second (and subsequent) times it does not work, because when you specify a function to be invoked by setTimeout, it is invoked by the 'window' context. This is because you are passing a reference to the step function, and context is not included in that reference.

Instead, you can maintain context by calling it from the correct scope, from inside an anonymous method:

var self = this;
setTimeout(function(){ self.step(); }, 1000);

Others pointed out the calling context issues, but here's a different solution:

setTimeout( this.step.bind( this ), 1000 );

This uses the ECMAScript 5 bind()[docs] method to send a function with the calling context bound to whatever you pass as the first argument.


If support for JS environments that don't support .bind() is needed, the documentation link I provided gives a solution that will be sufficient for most cases.

From the docs:

if (!Function.prototype.bind) {
    Function.prototype.bind = function (oThis) {
        if (typeof this !== "function") // closest thing possible to the ECMAScript 5 internal IsCallable function
        throw new TypeError("Function.prototype.bind - what is trying to be fBound is not callable");
        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP = function () {},
            fBound = function () {
                return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));
            };
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    };
}

This will add the .bind() shim to all functions via Function.prototype if it doesn't already exist.

I'm fairly sure that anything executed by setTimeout happens in global scope, so the reference to this no longer points to your function, it points to window.

To fix it, just cache this as a local variable inside step, then reference that variable in your setTimeout call:

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    var stepCache = this;
    setTimeout(function () { stepCache.step() }, 1000);
}

This is a closure issue as indicated by @SLaks.

Try this:

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    var self = this;
    this.render();
    setTimeout(function() {self.step();}, 1000);
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论