Kurt Peek
Kurt Peek

Reputation: 57471

Adjusting the scope of the callback function of onclick

I'm trying to solve a programming exercise in which there are several bugs to be fixed. Ultimately, it is supposed to represent an animation with a grid of cells where at each time step, if a cell has exactly three neighbors which are alive (each cell has 8 neighbors), it 'comes to life', and if it has less than 2 or more than 3 neighbors, it 'dies' (where the neighbors 'wrap around' the grid). The initial script is as follows:

<html>
<head>
<script type="text/javascript">
var Board;
var xsize = 10;
var ysize = 10;

var dead = 0;
var alive = 1;

function Neighbors(Board, x, y)
{
    var n = 0
    for(dx=-1;dx < 1; ++dx)
        for(dy=-1;dy < 1; ++dy)
        {
            var ax = x+dx;
            var ay = y+dy;
            if(Board[ax][ay]==alive) ++n;
        }
    return n;
}

function Kill(Board,x,y)
{
    if(Board[x][y] == alive)
        Board[x][y] = dead;
}

function MakeLive(Board,x,y)
{
    if(Board[x][y] == dead)
        Board[x][y] = alive;
}

function NextStep(Board)
{
    for(var x = 0; x <= xsize; ++x)
    {
        for(var y = 0; y <= ysize; ++x)
        {
            n = Neighbors(Board,x,y);
            if(n=3) MakeLive(Board,x,y);
            if((n<2)||(n>3)) Kill(Board,x,y);
        }
    }
}

function DrawBoard(Board)
{
    var Text = "";
    for(var y = 0; y < ysize; ++y)
    {
        for(var x = 0; x < xsize; ++x)
            Text += Board[x][y]==alive ? "o":"_";
        Text += "<br/>";
    }
    document.getElementById("board").innerHTML = Text;
}

function Main()
{
    // *** Change this variable to choose a different baord setup from below
    var BoardSetup = "blinker";

    Board = new Array(xsize);
    for(var x = 0; x < xsize; ++x)
    {
        Board[x] = new Array(ysize);
        for(var y = 0; y < ysize; ++y)
            Board[x][y] = 0;
    }

    if(BoardSetup == "blinker")
    {
        Board[1][0] = 1;
        Board[1][1] = 1;
        Board[1][2] = 1;
    }
    else if(BoardSetup == "glider")
    {
        Board[2][0] = 1;
        Board[2][1] = 1;
        Board[2][2] = 1;
        Board[1][2] = 1;
        Board[0][1] = 1;
    }
    else if(BoardSetup == "flower")
    {
        Board[4][6] = 1;
        Board[5][6] = 1;
        Board[6][6] = 1;
        Board[7][6] = 1;
        Board[8][6] = 1;
        Board[9][6] = 1;
        Board[10][6] = 1;
        Board[4][7] = 1;
        Board[6][7] = 1;
        Board[8][7] = 1;
        Board[10][7] = 1;
        Board[4][8] = 1;
        Board[5][8] = 1;
        Board[6][8] = 1;
        Board[7][8] = 1;
        Board[8][8] = 1;
        Board[9][8] = 1;
        Board[10][8] = 1;
    }

    DrawBoard(Board);
}
</script>
</head>
<body onload="Main()">
<div id="board">
</div>
<a href="#IGoNowhere" onclick="NextStep(Board);DrawBoard(Board);">Next -></a>
</body>
</html>

The problem is that if I press the 'Next' button, in the console I see the following error:

Uncaught TypeError: Cannot read property '-1' of undefined
    at Neighbors (life_broken__281_29.html:19)
    at Function.NextStep (life_broken__281_29.html:42)
    at HTMLAnchorElement.onclick (life_broken__281_29.html:117)
Neighbors @ life_broken__281_29.html:19
NextStep @ life_broken__281_29.html:42
onclick @ life_broken__281_29.html:117

The problem, I believe, is that the Board is defined in the Main() function, which is not in the scope of the onclick callback function.

My initial approach was to move the initialization of the Board outside of the Main() function, making it a global variable, and removing Board from all function calls. This does not seem like an elegant approach, however. Instead, I tried using Function.prototype.call() as follows:

<a href="#IGoNowhere" onclick="NextStep.call(Main, Board); DrawBoard.call(Main, Board);">Next -></a>

Further, I implemented a wrapAround function to avoid the indices going out of bounds:

function Neighbors(Board, x, y)
{
    var n = 0
    for(dx=-1;dx < 1; ++dx)
        for(dy=-1;dy < 1; ++dy)
        {
            var ax = x+dx;
            var ay = y+dy;
            ax = wrapAround(ax, xsize);
            ay = wrapAround(ay, ysize);
            if(Board[ax][ay]==alive) ++n;
        }
    return n;
}

function wrapAround(coordinate, size) {
    var result = coordinate % size;
    if (result < 0) {
        result += size;
    }
    return result;
}

However, now I get a new error:

life_broken__281_29.html:42 Uncaught TypeError: Cannot read property '0' of undefined
    at MakeLive (life_broken__281_29.html:42)
    at Function.NextStep (life_broken__281_29.html:53)
    at HTMLAnchorElement.onclick (life_broken__281_29.html:127)

Apparently, the Neighbors function is now not raising any errors, but the next function in NextStep, MakeLive, is. This I don't understand however because they are both defined at the same 'level' and have similar invocations in NextStep. Can anyone explain what the issue is here?

Update

Indeed Board is declared in the global scope, so there was no need for Function.prototype.call(). (I'm used to Python where declaration and definition are always in the same place). I also changed the Boolean expression to (x === 3).

However, for some reason x is still going up to 10 even if I replace the <= by a <. Here is the updated code, with a console.log statement for debugging:

<html>
<head>
<script type="text/javascript">
var Board;
var xsize = 10;
var ysize = 10;

var dead = 0;
var alive = 1;

function Neighbors(Board, x, y)
{
    var n = 0
    for(dx=-1;dx < 1; ++dx)
        for(dy=-1;dy < 1; ++dy)
        {
            var ax = x+dx;
            var ay = y+dy;
            ax = wrapAround(ax, xsize);
            ay = wrapAround(ay, ysize);
            if(Board[ax][ay]==alive) ++n;
        }
    return n;
}

function wrapAround(coordinate, size) {
    var result = coordinate % size;
    if (result < 0) {
        result += size;
    }
    return result;
}

function Kill(Board, x, y)
{
    if (Board[x][y] == alive)
        Board[x][y] = dead;
}

function MakeLive(Board, x, y)
{
    if (Board[x][y] == dead)
        Board[x][y] = alive;
}

function NextStep(Board)
{
    for(var x = 0; x < xsize; ++x)
    {
        for(var y = 0; y < ysize; ++x)
        {
            n = Neighbors(Board,x,y);
            console.log("x = " + x + ", y = " + y + ", n = " + n);
            if (n===3) MakeLive(Board,x,y);
            if ((n<2)||(n>3)) Kill(Board,x,y);
        }
    }
}

function DrawBoard(Board)
{
    var Text = "";
    for(var y = 0; y < ysize; ++y)
    {
        for(var x = 0; x < xsize; ++x)
            Text += Board[x][y]==alive ? "o":"_";
        Text += "<br/>";
    }
    document.getElementById("board").innerHTML = Text;
}

function Main()
{
    // *** Change this variable to choose a different baord setup from below
    var BoardSetup = "blinker";

    Board = new Array(xsize);
    for(var x = 0; x < xsize; ++x)
    {
        Board[x] = new Array(ysize);
        for(var y = 0; y < ysize; ++y)
            Board[x][y] = 0;
    }

    if(BoardSetup == "blinker")
    {
        Board[1][0] = 1;
        Board[1][1] = 1;
        Board[1][2] = 1;
    }
    else if(BoardSetup == "glider")
    {
        Board[2][0] = 1;
        Board[2][1] = 1;
        Board[2][2] = 1;
        Board[1][2] = 1;
        Board[0][1] = 1;
    }
    else if(BoardSetup == "flower")
    {
        Board[4][6] = 1;
        Board[5][6] = 1;
        Board[6][6] = 1;
        Board[7][6] = 1;
        Board[8][6] = 1;
        Board[9][6] = 1;
        Board[10][6] = 1;
        Board[4][7] = 1;
        Board[6][7] = 1;
        Board[8][7] = 1;
        Board[10][7] = 1;
        Board[4][8] = 1;
        Board[5][8] = 1;
        Board[6][8] = 1;
        Board[7][8] = 1;
        Board[8][8] = 1;
        Board[9][8] = 1;
        Board[10][8] = 1;
    }

    DrawBoard(Board);
}
</script>
</head>
<body onload="Main()">
<div id="board">
</div>
<a href="#IGoNowhere" onclick="NextStep(Board); DrawBoard(Board);">Next -></a>
</body>
</html>

and here is the result of the console when I click 'Next':

x = 0, y = 0, n = 0
life_broken__281_29.html:53 x = 1, y = 0, n = 1
life_broken__281_29.html:53 x = 2, y = 0, n = 0
life_broken__281_29.html:53 x = 3, y = 0, n = 0
life_broken__281_29.html:53 x = 4, y = 0, n = 0
life_broken__281_29.html:53 x = 5, y = 0, n = 0
life_broken__281_29.html:53 x = 6, y = 0, n = 0
life_broken__281_29.html:53 x = 7, y = 0, n = 0
life_broken__281_29.html:53 x = 8, y = 0, n = 0
life_broken__281_29.html:53 x = 9, y = 0, n = 0
life_broken__281_29.html:53 x = 10, y = 0, n = 0
life_broken__281_29.html:36 Uncaught TypeError: Cannot read property '0' of undefined
    at Kill (life_broken__281_29.html:36)
    at NextStep (life_broken__281_29.html:55)
    at HTMLAnchorElement.onclick (life_broken__281_29.html:128)

I'm a bit nonplussed why this is happening because a simple for loop in this fashion does work:

for (var i = 0; i < 10; ++i) {
    console.log("i = " + i);
}
VM158:2 i = 0
VM158:2 i = 1
VM158:2 i = 2
VM158:2 i = 3
VM158:2 i = 4
VM158:2 i = 5
VM158:2 i = 6
VM158:2 i = 7
VM158:2 i = 8
VM158:2 i = 9
undefined

Is the console somehow using a cached version of the old code? (I'm using the Live Preview in Brackets).

Update 2

This is because I should use a post-increment instead of a pre-increment (cf. http://jsforallof.us/2014/07/10/pre-increment-vs-post-increment/). Changing the ++x to x++ solved the problem.

Upvotes: 0

Views: 79

Answers (2)

JohanP
JohanP

Reputation: 5472

if(n=3) MakeLive(Board,x,y);, your n = 3 should be n === 3, I'm sure you don't want to assign 3 to n which would cause a truthy value, which will call MakeLive(Board,x,y); every time.

Also, in NextStep you have your x and y go all the way up to xsize and ysize (<=) whereas everywhere else you use <, think that causes your undefined value in Board[x]

Upvotes: 1

Barmar
Barmar

Reputation: 781068

The error has nothing to do with variable scope. Board is a global variable, so it's accessible to any function.

Your original problem was because you were accessing outside the Board array when x = 0 and dx = -1, and you fixed that with your wrapAround() function.

The next problem is that your loops in NextStep go too far. The row indexes go from 0 to xsize-1 and the columns go from 0 to ysize-1. But the loop there uses x <= xsize and y <= ysize, so it will try to access Board[xsize], which doesn't exist. Change those <= to <, just like the loop in Main().

Upvotes: 1

Related Questions