0

I'm trying to figure out why my Google Chrome console is giving me the error "undefined is not a function." I have a hunch, but maybe I'm on the wrong track. My function boxCollision(...) is defined at the bottom of my class. Nearer to the top I have a statement

    if (this.boxCollision(this.food.getBBox(), this.body[0].getBBox()))
            this.food.translate(this.linkSize, 0);

the first line of which is causing the error I mentioned. I think that's maybe because I haven't yet defined boxCollision, so it's essentially nonexistent. Is that right? The getBBox() functions are recognized because they're from an external JavaScript file.

    function snakegame(C, C_w, C_h, spd)
    {
            /* NOTE TO SELF: C is a Raphel object. Can't find a method to return the height
               and width of a Raphael object in the documentation: 
               http://raphaeljs.com/reference.html#Raphael.
               Using C_h and C_w for now, but should probably change it later.
            */


            this.linkSize = 50; /* size of a snake unit, in pixels; must divide C_h and C_w */


            this.link = C.rect(C_h/2, C_w/2, this.linkSize, this.linkSize);
            this.link.attr("fill", "#E9E581");
            this.body = [this.link];

            this.food = C.rect(randInt(0,C_w/this.linkSize-1) * this.linkSize, randInt(0,C_h/this.linkSize-1) * this.linkSize, this.linkSize, this.linkSize);
            if (this.boxCollision(this.food.getBBox(), this.body[0].getBBox()))
                this.food.translate(this.linkSize, 0);
            this.food.attr("fill","#B43535");

            this.maxSnakeSize = C_h * C_w / (this.linkSize * this.linkSize);

            /* On instantiation, the snake direction is down and has 1 link */
            this.dy = 0;
            this.dx = 0;


            this.score = 0;

            /* Event listener for changing the direction of the
               snake with arroy keys on the keyboard
            */
            this.redirect = function(dirnum)
            {
                switch (dirnum)
                {
                    /*
                        dirnum corresponds to
                        1 ---> right
                        2 ---> down
                        3 ---> left
                        4 ---> up
                    */
                    case 1: 
                        this.dx = this.linkSize;
                        this.dy = 0;
                        break;

                    case 2:
                        this.dx = 0;
                        this.dy = this.linkSize;
                        break;

                    case 3:
                        this.dx = -this.linkSize;
                        this.dy = 0;
                        break;

                    case 4:
                        this.dx = 0;
                        this.dy = -this.linkSize;
                        break;

                    default: /* never happens */
                        break;
                }

            }
            this.move = function()
            {

                if (this.body.length == this.maxSnakeSize)
                {
                    this.destruct();
                    return;
                }

                var addLink = false;
                var BBhead = this.body[0].getBBox();
                if (this.hitWall(BBhead) || this.hitSnake(BBhead))
                {
                    document.getElementById("snakescorediv").innerHTML = "<p>GAME OVER!</p><p>Score: "+ this.score +"</p>";
                    this.destruct();
                    return;
                }
                var BBfood = this.food.getBBox();
                if (this.boxCollision(BBhead, BBfood))
                {
                    this.moveFood();
                    this.score += 10;
                    document.getElementById("snakescorediv").innerHTML = this.score.toString();
                    addLink = true;
                }
                if (addLink)
                    this.body.push(this.body[this.body.length - 1].clone());
                for (var i = this.body.length - 1; i > 0; --i)
                {
                    var prevBB = this.body[i-1].getBBox();
                    var thisBB = this.body[i].getBBox();
                    this.body[i].translate(prevBB.x-thisBB.x, prevBB.y-thisBB.y)
                }
                this.body[0].translate(this.dx, this.dy);

            }

            this.mover = setInterval(this.move.bind(this), spd);   

            this.hitWall = function(bb)
            {
                return bb.x < 0 || bb.x2 > C_w || bb.y < 0 || bb.y2 > C_h;
            }

            this.hitSnake = function(bb)
            {
                var retval = false;
                for (var i = 1, j = this.body.length; i < j; ++i)
                {
                    var thisbb = this.body[i].getBBox();
                    if (this.boxCollision(bb, thisbb))
                    {
                        retval = true;
                        break;
                    }
                }
                return retval;
            }

            this.moveFood = function()
            {   
                var bbf = this.food.getBBox(); // bounding box for food
                do {
                /* tx, ty: random translation units */
                tx = randInt(0, C_w / this.linkSize - 1) * this.linkSize - bbf.x;
                ty = randInt(0, C_h / this.linkSize - 1) * this.linkSize - bbf.y;
                 // translate copy of food
                this.food.translate(tx, ty);
                bbf = this.food.getBBox(); // update bbf
                } while (this.hitSnake(bbf));

            }

            this.boxCollision = function(A, B)
            {
                return A.x == B.x && A.y == B.y;
            }


            this.destruct = function()
            {
                clearInterval(this.mover); 
                for (var i = 0, j = this.body.length; i < j; ++i)
                {
                    this.body[i].removeData();
                    this.body[i].remove();
                }
                this.food.removeData();
                this.food.remove();
                this.score = 0;
            }

    }

2 Answers2

1

Put the methods on the prototype to avoid this issue.

This won't work:

function Ctor() {
  this.init()
  this.init = function() {
    console.log('init')
  }
}

var inst = new Ctor // Error: undefined is not a function

But this will:

function Ctor() {
  this.init()
}

Ctor.prototype.init = function() {
  console.log('init')
}

var inst = new Ctor // init
elclanrs
  • 92,861
  • 21
  • 134
  • 171
0

Javascript parses code in two steps: compilation and evaluation.

The first step is compilation. In this step all definitions are compiled but no statement or expressions are evaluated. What this means is that definitions such as:

function a () {}

and:

var x

gets compiled into memory.

In the evaluation phase the javascript interpreter finally starts executing. This allows it to process operators which makes it possible to execute statements and expressions. It is in this step that variables get their values:

var x = 10;
    ^    ^
    |    |______ this part now gets assigned to `x` in the evaluation phase
    |
 this part was processed in the compilation phase

What this means is that for function expressions:

var x = function () {}

while both the variable and function body are compiled in the compilation phase, the anonymous function is not assigned to the variable until the evaluation phase. That's because the = operator is only executed in the evaluation phase (during the compilation phase all variables are allocated memory and assigned the value undefined).

Both the compilation phase and evaluation phase happen strictly top-down.

What some call "hoisting" is simply the fact that the compilation phase happen before the evaluation phase.

One work-around is to simply use a function definition instead of a function expression. Javascript support inner functions so a function defined in another function doesn't exist in the global scope:

        function boxCollision (A, B) {
            return A.x == B.x && A.y == B.y;
        }
        this.boxCollision = boxCollision;

Then you can use it at the top of your constructor:

if (boxCollision(this.food.getBBox(), this.body[0].getBBox()))
        this.food.translate(this.linkSize, 0);

Note that you can't use this.boxCollision because it's still undefined when you call it.

Another obvious work-around is to of course assign this.boxCollision = function (){} at the top before using it.

Or you could even assign it to the constructor's prototype. Or you can have an init function that gets called at the top (note: function, not method - again the use of a definition instead of a function expression make use of "hoisting").

There are many ways to get around this. But it's useful to know why it's happening to understand what works and what doesn't.

See my answer to this related question for more examples of this behavior: JavaScript function declaration and evaluation order

Community
  • 1
  • 1
slebetman
  • 109,858
  • 19
  • 140
  • 171