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 badges6 Answers
Reset to default 6When 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);
}