Is there a way to use named arrow function to maintain lexical scope inside an object?

Viewed 44

Description: I am using window.addEventListener() and want to call a named function (not an anonymous function) as I want to be able to use removeEventListener at a later stage.. However, the console.log inside the eventHandler which is an arrow function reveals that "this" still points to the window object instead of game object.

PS: Ignore the other parts of the code as I was refactoring this, and rest might be still incomplete, I know I can use constructors, but I am in a learning phase and before I study constructors I wanted to see if this can be done without the constructor

function makeGameObject() {

  return {

    score: 0,
    level: 1,


    start() {
      for (let coin of coins) {
        this.moveCoin(coin);
      }
      window.addEventListener("keydown", this.gameOn)
    },

    stop() {
      window.removeEventListener("keydown", this.gameOn);

    },


    gameOn: (evt) => {
      console.log(this);
      if (evt.key.toUpperCase() === "W" || evt.key === "ArrowUp") {
        this.moveObject(player, 30, 'up');
      } else if (evt.key.toUpperCase() === "S" || evt.key === "ArrowDown") {
        this.moveObject(player, 30, 'down');
      } else if (evt.key.toUpperCase() === "A" || evt.key === "ArrowLeft") {
        this.moveObject(player, 30, 'left');
        player.style.transform = 'scale(-1,1)';
      } else if (evt.key.toUpperCase() === "D" || evt.key === "ArrowRight") {
        this.moveObject(player, 30, 'right');
        player.style.transform = 'scale(1,1)';
      }

      for (let coin of coins) {
        if (this.isTouching(player, coin)) {
          this.moveCoin(coin);
          score++;
          h1.innerText = score;
        }
      }
    },

here is the call for the function:

const player = document.querySelector("#player");
const coins = document.querySelectorAll(".coin");
const body = document.querySelector("body");
const h1 = document.querySelector("h1");

const game = makeGameObject();
game.start();
1 Answers

One way to do it is to dynamically create a bound version of gameOn() and use that instead:

{
  // ...

  start() {
    for (let coin of coins) {
      this.moveCoin(coin);
    }
    if (this.boundGameOn === undefined) {
      this.boundGameOn = this.gameOn.bind(this);
    }
    window.addEventListener("keydown", this.boundGameOn)
  },

  stop() {
    window.removeEventListener("keydown", this.boundGameOn);
  },

  // ...
}

Ideally you'd do this in a constructor. If you have a constructor instead of an object literal you can do:

function GameObject () {
  this.boundGameOn = this.gameOn.bind(this)
}

GameObject.prototype = {
  // rest of code ..
}

In fact, in React applications it is not uncommon to see this design pattern:

function GameObject () {
  this.gameOn = this.gameOn.bind(this); // MAGIC!!
}

GameObject.prototype = {
  // ...

  start() {
    for (let coin of coins) {
      this.moveCoin(coin);
    }
    window.addEventListener("keydown", this.gameOn)
  },

  stop() {
    window.removeEventListener("keydown", this.gameOn);
  },

  // ...
}

The MAGIC line ensures that the this inside gameOn() always points to the game object because you're overwriting it with a bound version of itself.

This looks slightly cleaner in ES6 class syntax (only slightly, I personally have no preference for either syntax):

class GameObject {

  constructor () {
    this.gameOn = this.gameOn.bind(this); // MAGIC!!
  }

  // ...

  start() {
    for (let coin of coins) {
      this.moveCoin(coin);
    }
    window.addEventListener("keydown", this.gameOn)
  }

  stop() {
    window.removeEventListener("keydown", this.gameOn);
  }

  // ...
}

With the experimental class property syntax proposed for ES7 it's even simpler: you can just use an arrow function (don't use this directly for now, mid-2020, because Safari does not support this but if you use Babel or Typescript you can compile down to ES6):

class GameObject {
  // ...

  start = () => {
    for (let coin of coins) {
      this.moveCoin(coin);
    }
    window.addEventListener("keydown", this.gameOn)
  }

  stop = () => {
    window.removeEventListener("keydown", this.gameOn);
  }

  // ...
}

In this case this is bound by the arrow function.

Related