0

I'm trying to make a simple tic tac toe game to learn and practice JavaScript OOP and am having some difficulties.

I would like to reference the game.turn value when firing the game.handleClick function. I know that using the this keyword references the scope of the thing being called, and in handleClick, the this refers to the game-tile-x being clicked. How can I reference object values that are outside of the 'handleClick' function scope?

Any help would be deeply appreciated!

<div id="game-board">
    <div id="game-tile-1" class="game-tile"></div>
    <!-- divs 2-8 -->
    <div id="game-tile-9" class="game-tile"></div>
</div>
function Game() {
    this.tiles = document.getElementsByClassName('game-tile'),
    this.turn = 0;

    this.init = function() {
        for (let i = 0; i < this.tiles.length; i++) {
            // reset game tiles
            this.tiles[i].innerHTML = '';

            // setup event listeners
            this.tiles[i].addEventListener('click', this.handleClick);
        }
    };
    this.handleClick = function() {
        let id = parseInt(this.id.replace('game-tile-', ''));
        console.log(id); // returns 0, 1, 2, etc.
        console.log(this.turn); // returns undefined
    };
}

let game = new Game();
game.init();
1
  • 1
    it's common to set a variable to this so you can reference it later when it's out of scope. ex: let ctx = this; this.init = function(){....ctx.tiles[i]} Commented May 22, 2019 at 21:02

4 Answers 4

1

here a basic solution

function Game() {
    this.tiles = document.getElementsByClassName('game-tile'),
    this.turn = 0;

    this.init = function() {
        let tile;
        for (let i = 0; i < this.tiles.length; i++) {
            tile = this.tiles[i];
            // reset game tiles
            tile.innerHTML = '';

            // setup event listeners
            // don't mix this for the node & this for the game
            // let's bind the context to the game, and the first param to the tile
            tile.addEventListener('click', handleClick.bind(this, tile));
        }
    };

    function handleClick(tile) {
        let id = parseInt(tile.id.replace('game-tile-', ''));
        console.log(id); // returns 0, 1, 2, etc.
        console.log(this.turn); // returns undefined
    };
}

let game = new Game();
game.init();
Sign up to request clarification or add additional context in comments.

2 Comments

This solution works perfect for me! Would you be able to explain the difference between an object function with and without the this keyword? Example: this.myFunction = function() {} versus function myFunction() {}?
Function Declaration: function foo() {} - declares the function in the function scope WITH its implementation - makes it directly available from the top of the scope ("hoisting") - the this context is not defined but can be assigned on call. If I used var foo = function() {} (function expression), foo would be declared and hoisted but it would remain undefined until the expression is evaluated. Assign a function (declaration or expression) to an object property (this.func = foo or obj.func = foo) AND call it FROM it (obj.func()), the this context will reference the obj.
1

You could use a curried function, and call the outer one with the game instance (this) then you can use it inside:

 this.tiles[i].addEventListener('click', this.handleClick(this));

 //...

this.handleClick = function(game) {
 return function() {
    let id = parseInt(this.id.replace('game-tile-', ''));
    console.log(id); // returns 0, 1, 2, etc.
    console.log(game.turn); 
 };
};

Comments

1

JavaScript's this value is determined at call time and how it's called. When you click on a tile, the browser reacts by invoking handleClick. The this value is usually the element clicked. this.turn is undefined because your element does not have a property turn.

What you do is store the value of this in a separate variable within scope. This way, you can refer to this without using the keyword this.

function Game() {
  // The value of "this" in a constructor is the instance of the constructor.
  // We store a reference of "this" in variable "foo" (could be anything really).
  const foo = this

  this.handleClick = function() {
    // Use "instance" instead of "this".
    console.log(foo.turn)
  };
}

Alternatively, you can use function.bind() to bind the value of this (i.e. create an identical function whose this value is already defined ahead of time, instead of at call time).

function Game() {
  this.init = function() {
    for (let i = 0; i < this.tiles.length; i++) {
      // Tell the browser that when it calls handleClick, the value
      // of "this" is the one we provided in bind.
      this.tiles[i].addEventListener('click', this.handleClick.bind(this))
    }
  }
  this.handleClick = function() {
    console.log(this.turn); // returns undefined
  };
}

const game = new Game()
game.init()

Note that this approach also relies on how init() is called, because it also determines what init's this value is. If I call it differently, like the following:

const game = new Game()
const init = game.init()
init()

init's this will be window (normal mode) or undefined (strict mode), which will cause the binding of handleClick to use that value.

1 Comment

The second version won't work for the usecase shown by the OP.
0

There's two ways you can do this, either by saving this to a variable you will use later (we call this a closure):

for instance:

const self = this;
this.handleClick = function() {
    const id = parseInt(this.id.replace('game-tile-', ''));
};

Or by calling bind, which will bind the this object of the function to the this of the current scope:

this.handleClick = function() {
    const id = parseInt(this.id.replace('game-tile-', ''));
};
this.handleClick = this.handleClick.bind(this);

PS:

If you can use ES6 Arrow functions:

this.handleClick = () => {
    // this will point to the scope where the function is declared
}

If you can use ES6 classes:

class Game(){
    constructor(){
        this.tiles = document.getElementsByClassName('game-tile'),
        this.turn = 0;
        for (let i = 0; i < this.tiles.length; i++) {
        // reset game tiles
        this.tiles[i].innerHTML = '';

        // setup event listeners
        this.tiles[i].addEventListener('click', this.handleClick);
    }

    handleClick = () => {
        let id = parseInt(this.id.replace('game-tile-', ''));
        console.log(id); // returns 0, 1, 2, etc.
        console.log(this.turn); // returns undefined
    }
}

It is better to use ES6 syntax because it is easier to understand for both the reader and the machine. If you are worried about browser compability, use a transpiler like BabelJS

1 Comment

The second way won't work as the OP wants to access the clicked element too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.