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 trialDavid Accomazzo
15,986 PointsScope 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?
4 Answers
Dave StSomeWhere
19,870 PointsPretty 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.
Liam Clarke
19,938 PointsHi 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
David Accomazzo
15,986 PointsEveryone,
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;
}
}
David Accomazzo
15,986 PointsThanks, everyone!
Dave StSomeWhere
19,870 PointsDave StSomeWhere
19,870 PointsHard 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.