Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript

Scope with "this" keyword and functions in Javascript

Hi,

I am trying to write a simple JS tic tac toe game. I am having some trouble with assigning a click event to the squares of the game grid.

At first, I was assigning a "click" event listener to each square on the grid, and it worked:

var squares             = document.querySelectorAll('.square'),
square,
turn                    = "X",
turnTellerContainer     = document.querySelector('.turn-teller'),
turnTeller              = document.getElementById('x-or-o'),
turns                   = 0,
activeGame              = true;


// Initializing the game by adding the event listener to the squares

for (var i=0; i < squares.length; i++) {
    square = squares[i];

    // Adding the event listener
    square.addEventListener( 'click', function  {

    // Check if all the turns have been taken
                //if so, stop the game and give an alert.
        if (turns == 9) {
                return;
        }


        // Fill box with "X" or "O", depending on the value of the "turn" variable
        if (this.innerHTML.length === 3 && turn == "X" ) {
                this.innerHTML =  "X";
                turn = "O"
                turns++;
        }  else if (this.innerHTML.length === 0 && turn === "O") {
           this.innerHTML = "O";
           turn = "X";
           turns++;
        }
        // Tell us whose turn it is
        turnTeller.innerHTML = whoseTurn();
        if (turns == 9) {
                document.querySelector('#alertBox').innerHTML = "The Game Is Over!";
                turnTellerContainer.style.display = "none";
        }
        // Check if there's a winner
        if( checkForWinner() ) {
                document.querySelector('#alertBox').innerHTML = checkForWinner();
                toggleClicks();
        }

};

I decided that if I saved the click function to a variable, it would be more readable. So I saved the click function in its own separate function.

var squares             = document.querySelectorAll('.square'),
square,
turn                    = "X",
turnTellerContainer     = document.querySelector('.turn-teller'),
turnTeller              = document.getElementById('x-or-o'),
turns                   = 0,
activeGame              = true;


// Initializing the game by adding the event listener to the squares

for (var i=0; i < squares.length; i++) {
    square = squares[i];

    // Adding the event listener
    square.addEventListener( 'click', clickOnSquare() );
}


// The eventListener for clicking on a square

function clickOnSquare() {


    // Check if all the turns have been taken
                //if so, stop the game and give an alert.
        if (turns == 9) {
                return;
        }


        // Fill box with "X" or "O", depending on the value of the "turn" variable
        if (this.innerHTML.length === 3 && turn == "X" ) {
                this.innerHTML =  "X";
                turn = "O"
                turns++;
        }  else if (this.innerHTML.length === 0 && turn === "O") {
           this.innerHTML = "O";
           turn = "X";
           turns++;
        }
        // Tell us whose turn it is
        turnTeller.innerHTML = whoseTurn();
        if (turns == 9) {
                document.querySelector('#alertBox').innerHTML = "The Game Is Over!";
                turnTellerContainer.style.display = "none";
        }
        // Check if there's a winner
        if( checkForWinner() ) {
                document.querySelector('#alertBox').innerHTML = checkForWinner();
                toggleClicks();
        }

};

However, this doesn't work! For some reason, it seems to fire the function on page load, without waiting for a click, and I get the following error:

Uncaught TypeError: Cannot read property 'length' of undefined at clickOnSquare (game.js:36) at game.js:19

It's tripping up at "this" keyword.

I just spent 20 minutes trying to understand the scope of "this" and i'm way under my head. Can anyone help explain why this doesn't work?

Dave StSomeWhere
Dave StSomeWhere
19,870 Points

Hard to tell what going on, please check the Markdown cheatsheet below for posting your code.

Also your html and css would probably help. Also, any variables you need in clickOnSquare() will need to be passed. Also, the event object would probably be worth using instead of this to reference the actual square element that triggered the click event.

Here's a Pen to play with that might help explain the concept.

4 Answers

Dave StSomeWhere
Dave StSomeWhere
19,870 Points

Pretty nice David,

Here's another Pen for you to experiment with.

Didn't go to far, but it does put x's and o's in the squares and declares a winner.

Pretty sure I commented my changes. I did use simpler element.forEach with an arrow function to set the eventListener - I think it easier that doing the for loop and just use the event.target.

Also changed a bunch of "var"s to "let"s.

Hopefully this will get you rolling.

Also just guessed at the background color and font size variables in the css since you didn't include the variables for the import.

:tropical_drink: :palm_tree:

Hi David

This is because there are 2 scopes in javascript, local and global.

Your function clickOnSquare() is in the global scope, therefore without going into too much detail, just know it is kind of run immediately as JavaScript makes the function ready available with something called hoisting.

“this” doesn’t work because this on page load is referring to the window object as it is run straight away.

There’s a bunch of ways to get around this, you have certain design patterns, es6 class, object literals, function expressions, etc

I'll give you the design pattern example

Module pattern

(function () {
    // ... all your other code above this ( i kept it out to shorten this)

    var bindEvents = function() {
        square.addEventListener("click", _clickOnSquare );
    };

    var _clickOnSquare = function() {
            if (turns == 9) {
                return;
        }

        if (this.innerHTML.length === 3 && turn == "X" ) {
                this.innerHTML =  "X";
                turn = "O"
                turns++;
        }  else if (this.innerHTML.length === 0 && turn === "O") {
            this.innerHTML = "O";
            turn = "X";
            turns++;
        }

        turnTeller.innerHTML = whoseTurn();
        if (turns == 9) {
                document.querySelector('#alertBox').innerHTML = "The Game Is Over!";
                turnTellerContainer.style.display = "none";
        }

        if( checkForWinner() ) {
                document.querySelector('#alertBox').innerHTML = checkForWinner();
                toggleClicks();
        }
    };

    bindEvents();

})();

Function Expression

var clickOnSquare = function() {
    if (turns == 9) {
        return;
    }

    if (this.innerHTML.length === 3 && turn == "X" ) {
            this.innerHTML =  "X";
            turn = "O"
            turns++;
    }  else if (this.innerHTML.length === 0 && turn === "O") {
        this.innerHTML = "O";
        turn = "X";
        turns++;
    }

    turnTeller.innerHTML = whoseTurn();
    if (turns == 9) {
            document.querySelector('#alertBox').innerHTML = "The Game Is Over!";
            turnTellerContainer.style.display = "none";
    }

    if( checkForWinner() ) {
            document.querySelector('#alertBox').innerHTML = checkForWinner();
            toggleClicks();
    }
};
square.addEventListener("click", clickOnSquare );

Now that your scope is set out, see how the eventListener is below the clickOnSquare function

Everyone,

thanks for the replies. Here's the full code for reference. I'm going to go through these replies in depth later today, can't wait!

JS:

console.log('game.js has loaded');


var squares             = document.querySelectorAll('.square'),
square,
turn                    = "X",
turnTellerContainer     = document.querySelector('.turn-teller'),
turnTeller              = document.getElementById('x-or-o'),
turns                   = 0,
activeGame              = true;


// Initializing the game by adding the event listener to the squares

for (var i=0; i < squares.length; i++) {
    square = squares[i];

    // Adding the event listener
    square.addEventListener( 'click', clickOnSquare() );
}


// The eventListener for clicking on a square

function clickOnSquare() {


    // Check if all the turns have been taken
                //if so, stop the game and give an alert.
        if (turns == 9) {
                return;
        }


        // Fill box with "X" or "O", depending on the value of the "turn" variable
        if (this.innerHTML.length === 3 && turn == "X" ) {
                this.innerHTML =  "X";
                turn = "O"
                turns++;
        }  else if (this.innerHTML.length === 0 && turn === "O") {
           this.innerHTML = "O";
           turn = "X";
           turns++;
        }
        // Tell us whose turn it is
        turnTeller.innerHTML = whoseTurn();
        if (turns == 9) {
                document.querySelector('#alertBox').innerHTML = "The Game Is Over!";
                turnTellerContainer.style.display = "none";
        }
        // Check if there's a winner
        if( checkForWinner() ) {
                document.querySelector('#alertBox').innerHTML = checkForWinner();
                toggleClicks();
        }

};


// Check whose turn it is
function whoseTurn() {
    if ( turn === "X") {
        return "X";
    } else {
        return "O";
    }

}

// This function checks if there's a winner after each turn. It gets called whenver a square is clicked.
// Returns false if no winner detected, otherwise returns name of winner.

function checkForWinner() {
    var topLeft         = document.querySelector('.horizontal-1.vertical-1').innerHTML,
        topMiddle       = document.querySelector('.horizontal-1.vertical-2').innerHTML,
        topRight        = document.querySelector('.horizontal-1.vertical-3').innerHTML,
        middleLeft      = document.querySelector('.horizontal-2.vertical-1').innerHTML,
        middleMiddle    = document.querySelector('.horizontal-2.vertical-2').innerHTML,
        middleRight     = document.querySelector('.horizontal-2.vertical-3').innerHTML,
        bottomLeft      = document.querySelector('.horizontal-3.vertical-1').innerHTML,
        bottomMiddle    = document.querySelector('.horizontal-3.vertical-2').innerHTML,
        bottomRight     = document.querySelector('.horizontal-3.vertical-3').innerHTML;

    if ( topLeft ) {
        if (topLeft === topMiddle && topMiddle === topRight) {
            return "Player " + topLeft + " Wins!";
        } else if ( topLeft === middleMiddle && middleMiddle === bottomRight ) {
            return "Player " + topLeft + " Wins!";
        } else if ( topLeft === middleLeft && middleLeft === bottomLeft ) {
            return "Player " + topLeft + " Wins!";
        }
    }

    if ( topMiddle ) {
       if ( topMiddle === middleMiddle && middleMiddle === bottomMiddle ) {
           return "Player " + topMiddle + " Wins!";
       }
    }

    if ( topRight ) {
        if ( topRight === middleRight && middleRight === bottomRight ) {
           return "Player " + topRight + " Wins!";
        } else if (topRight === middleMiddle && middleMiddle === bottomLeft) {
            return "Player " + topRight + " Wins!";
        }
    }

    if (middleLeft) {
        if ( middleLeft == middleMiddle && middleMiddle == middleRight ) {
            return "Player " + middleLeft + " Wins!";
        }
    }

    if ( bottomLeft ) {
       if ( bottomLeft === bottomMiddle && bottomMiddle === bottomRight ) {
            return "Player " + bottomLeft + " Wins!";
       }
    }

    return false;

}



// Controlling the behavior of the reset button
// Clears the squares
// Clears the alert box
// Shows the turn teller container if it is hidden
var reset = document.getElementById('reset-button');
reset.addEventListener('click', function(e) {
    console.log("reset");
    e.preventDefault();
    turns = 0;
    var squares1 =  document.querySelectorAll('.square');
    for( var i = 0; i < squares1.length; i++) {
        squares1[i].innerHTML = "";
    }
    document.querySelector('#alertBox').innerHTML = "";
    turnTellerContainer.style.display = "block";
});


//Todo: Create function that disables squares when game is over.
//
function toggleClicks() {
    console.log('toggleClicks()');
    if ( checkForWinner() ) {
        for ( var i = 0; i < squares.length; i++) {
            squares[i].removeEventListener('click', click);
        }
    }
}

HTML:

<!DOCTYPE HTML>
<html>
<head>
<title>Tic Tac Toe</title>
<link rel="stylesheet" type="text/css" href="http://tictactoe.test/dist/css/game.min.css">
</head>

<body>
<div class="viewport">
    <div id="app">
        <h1 class="center">Tic Tac Toe</h1>
        <p class="center turn-teller">It's Player <span id="x-or-o"></span>'s turn!</span></p>
                <p class="center" id="alertBox"></p>
        <div class="flex-center flex-container board-container">
            <div class="flex-container flex-wrap board">
                <div class="square horizontal-1 vertical-1"></div>
                <div class="square horizontal-1 vertical-2"></div>
                <div class="square horizontal-1 vertical-3"></div>
                <div class="square horizontal-2 vertical-1"></div>
                <div class="square horizontal-2 vertical-2"></div>
                <div class="square horizontal-2 vertical-3"></div>
                <div class="square horizontal-3 vertical-1"></div>
                <div class="square horizontal-3 vertical-2"></div>
                <div class="square horizontal-3 vertical-3"></div>
            </div>
        </div>
                <div class="button-row">
                        <a href="#" id="reset-button">RESET GAME!</a>
                </div>
    </div>
</div>

<script type="text/javascript" src="/dist/js/game.js"></script>
</body>
</html>

SASS:

@import 'variables';

/* Utility Classes */

.flex-container {
    display: flex;
}

.flex-center {
    justify-content: center;
    align-items: center;
}

.flex-wrap {
    flex-wrap: wrap;
}

.center {
    text-align: center;
}

body {
    height: 100%;
    font-family: Open-Sans;
    background-color: $background;
}

#app {
    width: 90%;
    margin: 0 auto;
}

.square {
    border: 1px solid firebrick;
    min-width: 100px;
    max-width: 400px;
    width: 33%;
    height: 200px;
        display: flex;
    justify-content: center;
    align-items: center;
    font-size: $letterSize;
    &:hover {
        background-color: gray;
    }
}

Thanks, everyone!