What is `this` in event listeners?

29 Mar 2018 · by David DeSandro

I was just asked:

Hello desandro! I'm a new developer.

elem.addEventListener( startEvent, this );

I wonder what is this means. I'm confused. [edited]

Confusing, indeed! this in JavaScript is difficult to learn, and especially tricky to understand when used in event listeners.

Let's take a step back and look at a simplified example. Here is a demo for a mouse-draggable element.

var dragElem = document.querySelector('.draggable');

var x = 0;
var y = 0;
var dragStartX, dragStartY, pointerDownX, pointerDownY;

dragElem.addEventListener( 'mousedown', function( event ) {
  // keep track of start positions
  dragStartX = x;
  dragStartY = y;
  pointerDownX = event.pageX;
  pointerDownY = event.pageY;
  // add move & up events
  window.addEventListener( 'mousemove', onmousemove );
  window.addEventListener( 'mouseup', onmouseup );
});

function onmousemove( event ) {
  // how much has moved
  var moveX = event.pageX - pointerDownX;
  var moveY = event.pageY - pointerDownY;
  // add movement to position
  x = dragStartX + moveX;
  y = dragStartY + moveY;
  // position element
  dragElem.style.left = x + 'px';
  dragElem.style.top = y + 'px';
}

function onmouseup() {
  // remove move & up events
  window.removeEventListener( 'mousemove', onmousemove );
  window.removeEventListener( 'mouseup', onmouseup );
}

See the Pen Draggable, single element by Dave DeSandro (@desandro) on CodePen.

To start dragging, I first add a mousedown event listener. When triggered, I then add listeners for mousemove and mouseup. In onmousemove is where I calculate and set the element's position. In onmouseup, I remove mousemove and mouseup listeners to stop dragging.

This works just fine for a single element. Metafizzy plugins are designed to handle multiple instances on the same page.

var dragElems = document.querySelectorAll('.draggable');
for ( var i=0; i < dragElems.length; i++ ) {
  var dragElem = dragElems[i];
  // now what?...
}

One way to approach this is to wrap up the draggable code into its own big function and call that for each element. But I like to use classes with prototype to handle multiple instances of the same behavior.

// Dragger class
function Dragger( element ) {
  this.element = element;
  this.x = 0;
  this.y = 0;
  // doesn't work!
  this.element.addEventListener( 'mousedown', this.onmousedown );
}

Dragger.prototype.onmousedown = function( event ) {
  this.dragStartX = this.x;
  this.dragStartY = this.y;
  this.pointerDownX = event.pageX;
  this.pointerDownY = event.pageY;
  // doesn't work, again!
  window.addEventListener( 'mousemove', this.onmousemove );
  window.addEventListener( 'mouseup', this.onmouseup );
};

Now I need to add an event listener within the Dragger class functions. But there is a problem. I want to use this to reference the instance of the Dragger class within the event handler functions. But functions added with addEventListener will reference the bound element as this, not the function or object.

dragElem.addEventListener( 'mousedown', function() {
  console.log( this );
  // => Element
});

handleEvent

One solution is to use a little-known feature of browser JavaScript, handleEvent. An object with an handleEvent method will be triggered with when added with addEventListener. Within the handleEvent method, this will refer to the listener object, not the element.

var listener = {
  greeting: 'Hello ',
  handleEvent: function( event ) {
    console.log( this.greeting + event.type );
  },
};

dragElem.addEventListener( 'mousedown', listener );
// on mousedown...
// => 'Hello mousedown'

See simple handleEvent demo on CodePen.

The handleEvent method can be used for multiple events. You can specify logic by using event.type.

var listener = {
  greeting: 'Hello ',
  handleEvent: function( event ) {
    console.log( this.greeting + event.type );
  },
};

dragElem.addEventListener( 'mousedown', listener );
dragElem.addEventListener( 'mousemove', listener );
dragElem.addEventListener( 'mouseup', listener );
// on mousedown => 'Hello mousedown'
// on mousemove => 'Hello mousemove'
// on mouseup => 'Hello mouseup'

Back to the Dragger class. So now I add the handleEvent method to cooridate which event method to trigger. Then I can add this as the event listener.

// Dragger class
function Dragger( element ) {
  this.element = element;
  this.x = 0;
  this.y = 0;
  // add this as event listener, trigger handleEvent
  this.element.addEventListener( 'mousedown', this );
}

// trigger .ontype from event.type
// i.e. trigger onmousedown() from mousedown
Dragger.prototype.handleEvent = function( event ) {
  var method = 'on' + event.type;
  // call method if there
  if ( this[ method ] ) {
    this[ method ]( event );
  }
};

Dragger.prototype.onmousedown = function( event ) {
  this.dragStartX = this.x;
  this.dragStartY = this.y;
  this.pointerDownX = event.pageX;
  this.pointerDownY = event.pageY;
  // add this as event listener, trigger handleEvent
  window.addEventListener( 'mousemove', this );
  window.addEventListener( 'mouseup', this );
};

See the Pen Draggable event listeners, handleEvent by Dave DeSandro (@desandro) on CodePen.

So to the original question: this can be used as an event listener as it has a handleEvent method. That method then triggers other methods that match event.type, like onmousedown.

I learned the handleEvent technique back in 2010. I use it in all the Metafizzy plugins. But JavaScript has come a long way in that time.

bind this

Instead of adding circuitry through handleEvent, you can specify this with .bind().

The Function.prototype.bind() method lets you specify the value that should be used as this for all calls to a given function.

Using bind() has an additional benefit in that you can add multiple event listeners for the same event name.

this.element.addEventListener( 'click',
  this.onElementClick.bind( this ) );

this.button.addEventListener( 'click',
  this.onButtonClick.bind( this ) );

But, because bind() returns a new function, you will need to keep track of that function if you want to remove it later.

this.handleElementClick = this.onElementClick.bind( this );
this.handleButtonClick = this.onButtonClick.bind( this );
// add event listener
this.element.addEventListener( 'click', this.handleElementClick );
this.button.addEventListener( 'click', this.handleButtonClick );
// remove event listener
this.element.removeEventListener( 'click', this.handleElementClick );
this.button.removeEventListener( 'click', this.handleButtonClick );

Here's what the Dragger class looks like using bind().

function Dragger( element ) {
  this.element = element;
  this.x = 0;
  this.y = 0;
  // bind self for event handlers
  this.mousedownHandler = this.onmousedown.bind( this );
  this.mousemoveHandler = this.onmousemove.bind( this );
  this.mouseupHandler = this.onmouseup.bind( this );

  this.element.addEventListener( 'mousedown', this.mousedownHandler );
}

Dragger.prototype.onmousedown = function( event ) {
  this.dragStartX = this.x;
  this.dragStartY = this.y;
  this.pointerDownX = event.pageX;
  this.pointerDownY = event.pageY;

  window.addEventListener( 'mousemove', this.mousemoveHandler );
  window.addEventListener( 'mouseup', this.mouseupHandler );
};

See the Pen Draggable bind this by Dave DeSandro (@desandro) on CodePen.

Arrow functions

With the new ES6 hotness, you can specify this using arrow functions. Within an arrow function, this will refer to the enclosing object.

function Dragger( element ) {
  this.element = element;
  this.x = 0;
  this.y = 0;

  // event listeners, with arrow functions
  this.onmousedown = ( event ) => {
    this.dragStartX = this.x;
    this.dragStartY = this.y;
    this.pointerDownX = event.pageX;
    this.pointerDownY = event.pageY;

    window.addEventListener( 'mousemove', this.onmousemove );
    window.addEventListener( 'mouseup', this.onmouseup );
  };

  this.onmousemove = ( event ) => {
    var moveX = event.pageX - this.pointerDownX;
    var moveY = event.pageY - this.pointerDownY;
    this.x = this.dragStartX + moveX;
    this.y = this.dragStartY + moveY;
    this.element.style.left = this.x + 'px';
    this.element.style.top = this.y + 'px';
  };

  this.onmouseup = () => {
    window.removeEventListener( 'mousemove', this.onmousemove );
    window.removeEventListener( 'mouseup', this.onmouseup );
  };

  // add event listener
  this.element.addEventListener( 'mousedown', this.onmousedown );
}

See the Pen Draggable event listeners, arrow functions by Dave DeSandro (@desandro) on CodePen.

Personally, I'm not a fan of the arrow function technique for this scenario. It puts the method code inside another function, rather than outside on prototype. But I'm including it for completeness.

Each technique has its own pros and cons. handleEvent has served me well over the years, but I'm finding that I run into event name conflicts with big plugins like Flickity. So I'm starting to use out bind() a bit more. But then I miss the elegance of adding just this and not having to deal with extra event handler functions. Arrow functions, meanwhile, are just not for me.