3

I'm new to JavaScript. Hence this problem is a bit confusing. I'm trying to simply define a counter and increment it in a class method but its not behaving as I expect it to. Specifically console.log(this.tick_count); prints undefined.

JavaScript:

function Game() {
    this.fps = 50;
    this.ticks = 3;
    this.current_time = (new Date).getTime();
    this.draw_object = document.getElementById('game_canvas').getContext('2d');
    this.tick_count = 0;
}

Game.prototype.update = function (time) {
    this.current_time = time;
}

Game.prototype.draw = function () {
    this.draw_object.fillRect(10, 10, 55, 50);
}

Game.prototype.run = function () {
    self.setInterval(this.tick, 1000 / (this.fps * this.tick));
}

Game.prototype.tick = function () {
    this.tick_count++;
    console.log(this.tick_count);
}

function start_game() {
    var game_object = new Game();
    game_object.run();
}

HTML:

<body onload="start_game()">
    <canvas id="game_canvas" width="1024" height="1024"></canvas>
</body>

Coming from a Python background I find this behavior strange. How should I set up my class variables correctly?

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
jonathan topf
  • 7,897
  • 17
  • 55
  • 85
  • 2
    Where are you defining `self`? And `this` inside `setInterval` isn't what you think it is. – Evan Davis Sep 26 '13 at 16:25
  • possible duplicate of [Javascript setInterval scoping issue](http://stackoverflow.com/questions/11333311/javascript-setinterval-scoping-issue) – Evan Davis Sep 26 '13 at 16:26
  • 1
    Try `self.setInterval(this.tick.bind(this), 1000 / (this.fps * this.tick));` and see how it goes – PSL Sep 26 '13 at 16:27
  • @PSL That will still not work due to the lack of `self`. It is probably just a type due to his Python backgrounds. – jshthornton Sep 26 '13 at 16:49
  • @jshthornton lack of self what? what is self by the way. – PSL Sep 26 '13 at 16:49
  • @PSL in Python `self` is the `this` keyword-ish. – jshthornton Sep 26 '13 at 16:51
  • @jshthornton is this python or javascript. – PSL Sep 26 '13 at 16:52
  • @PSL This is javascript, but OP said he comes from a Python background, and he used `self` instead of this, which was probably just a typo, but it is causing issues. But to be honest, there shouldn't be a `this` there either. – jshthornton Sep 26 '13 at 16:54
  • @jshthornton See it working here. http://jsfiddle.net/hVjhL/ look at the console – PSL Sep 26 '13 at 16:56
  • @PSL It will work because most browsers keep a reference to window via `self` but it isn't pleasant at all as a lot of js developers use `self` as a temporary this reference. – jshthornton Sep 26 '13 at 17:00
  • @jshthornton yes ofcource, issue is nothing to do with with self. it is with the callback.. :) – PSL Sep 26 '13 at 17:01
  • @PSL agreed. But if you look at the top comment you can see that people are getting confused... – jshthornton Sep 26 '13 at 17:01

4 Answers4

10

This is what is happening.

Essentially you tick function is no longer running in the context of your game_object object. This might sound odd coming from a Python background but basically the this object is set to something else.

So what is it set to? Easy, the window object, how do we know this? Because setInterval's context is the window object.

Moving example as code will not format correctly below

Bind Example

setInterval(this.tick.bind(this), 1000 / (this.fps * this.tick)); //Native (JS v1.8+)
$.proxy(this.tick, this); //jQuery
_.bind(this.tick, this); //underscore / lodash

Explicit context example

Game.prototype.run = function () {  
    var _this = this;  
    setInterval(function() {  
        //So what is this at the moment? window.
        //Luckily we have a reference to the old this.
        _this.tick();  
    }, 1000 / (this.fps * this.tick));  
 };

You can get around this two ways.

  1. Bind your function to the object you want it to be on Bind JS v1.8 (Seeing as you're using canvas that shouldn't be an issue.
  2. Invoke the method explicitly with its context. (See above)
jshthornton
  • 1,284
  • 11
  • 29
4

Try

setInterval(this.tick.bind(this), 1000 / (this.fps * this.tick));
// without "self"

Thanks to PSL and TJ Crowder

Paul Rad
  • 4,820
  • 1
  • 22
  • 23
2

This will work:

setInterval(this.tick.bind(this), 1000 / (this.fps * this.tick));

As will this:

var self = this;
setInterval(function () {
    self.tick();
}, 1000 / (this.fps * this.tick));
Andy
  • 61,948
  • 13
  • 68
  • 95
0

Even though this has been answered I think you need to understand what this refers to. See this answer for more details.

If you would like to use closures instead of bind you can limit the scope by calling a function that returns a function (outside of the currently running function). This is so you can minimise the amount of variables that will be available to the closure. Sounds complicated but with a minor adjustment to your code you can do it:

Game.prototype.run = function () {
    //best not to define a closure here because the next variable
    //will be available to it and won't go out of scope when run
    //is finished
    var memoryEatingVar=new Array(1000000).join("hello world");;
    //note that this.tick(this) is not passing this.tick, it's invoking
    //it and the return value of this.tick is used as the callback
    //for the interval
    setInterval(this.tick(this), 1000 / (this.fps * this.tick));
}
//note the "me" variable, because the outer function Game.prototype.tick
//returns a inner function (=closure) the me variable is available in 
//the inner function even after the outer function is finished
Game.prototype.tick = function (me) {//function returning a function
  return function(){
    me.tick_count++;
    console.log(me.tick_count);
  }
}
Community
  • 1
  • 1
HMR
  • 37,593
  • 24
  • 91
  • 160