简体   繁体   中英

binding with es6 classes and jquery

working on a project I notice that this in class methods is not really attached to the instance, nor even the class, when accessed through jquery calls. Instead it is the window global that I am seeing, and that's not right.

So there are various methods to autobind that for you.. I tried a couple, and -- not with success. What is going on?

Here's the errors:

Uncaught TypeError: this.chooseAtRandom is not a function
    at move (pen.js:83)
move @ pen.js:83
pen.js:102 Uncaught TypeError: Cannot read property 'push' of undefined
    at UIController.animate (pen.js:102)
    at HTMLDivElement.<anonymous> (pen.js:131)
    at HTMLDivElement.dispatch (jquery.min.js:3)
    at HTMLDivElement.q.handle (jquery.min.js:3)
animate @ pen.js:102
(anonymous) @ pen.js:131
dispatch @ jquery.min.js:3
q.handle @ jquery.min.js:3
pen.js:83 Uncaught TypeError: this.chooseAtRandom is not a function
    at move (pen.js:83)

Here's my autobind method:

class Binder {
  getAllMethods(instance, cls) {
  return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
    .filter(name => {
      let method = instance[name]
      return !(!(method instanceof Function) || method === cls)
    })
  }
  bind(instance, cls) {
  Binder.getAllMethods(instance, cls)
    .forEach(mtd => {
      instance[mtd] = instance[mtd].bind(instance);
    })
  }
}

Note the Binder.getAllMethods call in there. If I change that to this.getAllMethods I get a problem here too. I'm wondering if codepen is breaking it

And here's me using it:

class SimonAudio {
  constructor() {
    this.soundsrcs = [
      'https://s3.amazonaws.com/freecodecamp/simonSound1.mp3',
      'https://s3.amazonaws.com/freecodecamp/simonSound2.mp3',
      'https://s3.amazonaws.com/freecodecamp/simonSound3.mp3',
      'https://s3.amazonaws.com/freecodecamp/simonSound4.mp3'
    ]
    this.players = this.soundsrcs.map(s => {
      let a = document.createElement('audio');
      a.setAttribute('src', s)
      return a
    })
    this.uiMap = {
      red: 0,
      green: 1,
      amarillo: 2,
      blue: 3
    }

    Binder.bind(this, SimonAudio)
  }
  play(uiId) {
    this.players[this.uiMap[uiId]].play()
  }
}

class Game {
  constructor(UI) {
    this.UI = UI
    this.uiMap = {
      red: 0,
      green: 1,
      amarillo: 2,
      blue: 3
    };
    this.dexUi = ['red', 'green', 'amarillo', 'blue']
    this.states = ['SHOWINGCHALLENGE', 'LISTENING']
    this.audio = new SimonAudio()
    this.moves = []
    this.gameStack = []
    this.inGame = false

    Binder.bind(this, Game)
  }

  start() {
    this.inGame = true
    this.gameStack.push(setTimeout(this.move, parseInt((Math.random() + 1) * 1000)))
  }
  restart() {
    this.moves = []
    this.gameStack.forEach(a => {
      clearTimeout(a)
    })
    this.gameStack = []
    this.inGame = false
  }
  playMoves() {
    let elf = this.UI
    this.moves.forEach(m =>
      this.gameStack.push(
        setTimeout(function() {
          elf.animate(m)
        }, parseInt((Math.random() + 1) * 500)))
    )
  }
  move() {
    let move = this.chooseAtRandom()
    this.moves.push(move)
    this.playMoves()
  }
  chooseAtRandom() {
    return this.dexUi[parseInt(Math.random() * 4)]
  }
}

class UIController {
  contructor() {
    this.animation_stack = []
    Binder.bind(this, UIController)
  }
  clear() {
    this.animation_stack.forEach(a => clearTimeout(a))
    this.animation_stack = []
  }
  animate(uiId) {
    $(`#${uiId}`).addClass('highlight')
    this.animation_stack.push(setTimeout(function() {
      $(`#${uiId}`).removeClass('highlight')
    }, 333))
  }
}

Then the jquery that is blowing that up:

$(function() {
  let UI = new UIController()
  let Simon = new Game(UI)

  $('#strict').click(function() {
    $(this).toggleClass('toggled')
  })

  $('#restart').click(function() {
    $('.controls .button').removeClass('disabled toggled')
    $('.game-buttons div').addClass('disabled')
    Simon.restart()
  })

  $('#start').click(function() {
    if (Game.inGame)
      return
    $('.game-buttons > div').addClass('hvr-radial-out')
    $(this).addClass('toggled')
    $('#strict').addClass('disabled')
    $('.game-buttons div').removeClass('disabled')
    Simon.start()
  })

  $('.game-buttons div').addClass('disabled')
    .click(function() {
      if ($(this).hasClass('disabled'))
        return
      let id = $(this).attr('id')
      UI.animate(id)
      Simon.audio.play(id)
    })

})

Uncaught TypeError: this.chooseAtRandom is not a function

OK, well let's look at the calling code

move() {
  let move = 
  this.moves.push(move)
  this.playMoves()
}

OK, let's look at the calling code for move now

start() {
  this.inGame = true
  this.gameStack.push(setTimeout(, parseInt((Math.random() + 1) * 1000)))
}

OK, right there you lose the this context you hope to preserve when the function actually fires. We can easily rewrite this in a couple of ways. But before I start copying/pasting another really bad part of your code, I have to fix that first.

// parseInt is for converting strings to numbers
// Math.random() gives you a number so you don't need to parse it
// change this
parseInt((Math.random() + 1) * 1000)

// to this
(Math.random() + 1) * 1000

OK, now let's fix your function context

// Option 1: use Function.prototype.bind
this.gameStack.push(setTimeout(this.move.bind(this), (Math.random() + 1) * 1000))

// Option 2: use a sexy ES6 arrow function
this.gameStack.push(setTimeout(() => this.move(), (Math.random() + 1) * 1000))

OK, now let's check if start is called with the proper context

$('#start').click(function() {
  if (Game.inGame)
    return
  $('.game-buttons > div').addClass('hvr-radial-out')
  $(this).addClass('toggled')
  $('#strict').addClass('disabled')
  $('.game-buttons div').removeClass('disabled')
  
})

OK ! The this in your start function will be set to the Simon instance of Game . I can't run the rest of your code so I don't know if there are other problems, but that should fix the this.chooseAtRandom is not a function error you were seeing before.

OK.

Binding as I understand it is typically used to bind a function or multiple functions to an object.

For example using JS's bind:

this.start.bind(this);

Or using lodash's bind/bindAll:

_.bind(this.start, this);
_.bindAll(this, ['start', 'restart']);

"this" in these cases would refer to an instance of the Game class. I believe this would ensure the bound method(s) would always be called within the context of the game instance.

Your approach doesn't quite appear to follow this usage.

I recommend using the lodash bind/bindAll implementations rather than trying to roll your own.

Resources

PS That's awesome you're making a Simon game!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM