skip to content

Besides a function, addEventListener accepts an object as its listener argument. What must that object provide, and what is `this` inside it when the event fires?

level: middleimportance: nice to knowfreq 16%

answer

  1. a listener need not be a function
  2. one method the browser looks for
  3. this is the object, not the element
  4. the instance is a stable reference
  5. looked up at dispatch, not registration

basics

~10 s

The object must have a handleEvent method. The browser calls listener.handleEvent(event), and inside that method this is the listener object itself, not the element. Pass the same object to removeEventListener to unregister it.

solid answer

~40 s

`addEventListener`'s second parameter is typed as `EventListener`, and the DOM lets you satisfy it with any object that has a `handleEvent` method rather than a bare function. On dispatch the browser looks up `handleEvent` on that object and calls it with the event, so `this` inside the method is the object — unlike a plain function listener, where `this` is the `currentTarget`. The practical use is a class that acts as its own listener: `el.addEventListener('click', this)` in the constructor, a `handleEvent(event)` method that switches on `event.type`, and `el.removeEventListener('click', this)` in the teardown. That sidesteps the whole `.bind(this)` identity problem, because the instance is a stable reference you already have. The lookup happens at dispatch time, so assigning `handleEvent` after registration still works.

code

javascript · 20 lines
javascript
class Toggle {
  constructor(el) {
    this.el = el;
    this.open = false;
    el.addEventListener('click', this); // the instance is the listener
  }
  handleEvent(event) {
    if (event.type === 'click') this.toggle();
  }
  toggle() {
    this.open = !this.open;
    this.el.textContent = this.open ? 'open' : 'closed';
  }
  destroy() {
    this.el.removeEventListener('click', this); // same reference, always matches
  }
}

const t = new Toggle(document.createElement('button'));
t.el.click(); // textContent becomes "open"

go deeper

for a junior

Recall that addEventListener accepts an object with a handleEvent method as its listener, and that the browser calls that method with the event object.

for a middle

Explain the this contrast — the listener object rather than currentTarget — and how registering an instance directly gives a stable reference that makes removeEventListener match reliably.

for a senior

Show where you would actually reach for it: a class-based widget or custom element that handles several event types in one place and whose teardown must be provably complete.

for a principal

Weigh it as a convention question — a single handleEvent switch centralises a component's event handling, but it is out of step with a codebase built on standalone handlers, so pick one idiom and hold the line.

## The EventListener interface Most developers only ever pass a function to `addEventListener`, but the DOM defines the second argument as an `EventListener` — a callback interface that a plain object can implement. The requirement is a single method: ```js const logger = { handleEvent(event) { console.log(event.type, this === logger); // "click true" } }; el.addEventListener('click', logger); ``` When the event is dispatched, the browser checks whether the registered listener is callable. If it is not, it looks up the `handleEvent` property and calls that, passing the event as the only argument. ## The `this` binding is the difference With a plain function listener, `this` inside the handler is the `currentTarget` — the element the listener is attached to. With an object listener, `this` is the **listener object**. That is precisely why the form is useful: a class instance registered as its own listener can reach its own fields and methods with no binding ceremony. ```js class Menu { constructor(root) { this.root = root; this.open = false; root.addEventListener('click', this); root.addEventListener('keydown', this); } handleEvent(event) { if (event.type === 'click') this.toggle(); if (event.type === 'keydown' && event.key === 'Escape') this.close(); } toggle() { this.open = !this.open; } close() { this.open = false; } destroy() { this.root.removeEventListener('click', this); this.root.removeEventListener('keydown', this); } } ``` Note that the element is still reachable — `event.currentTarget` gives you what `this` would have been for a function listener. ## Why it helps with removal Removal matches on the callback *reference*. The classic failure is registering `this.onClick.bind(this)`, because `bind` produces a new function each call and the removal never matches. The usual workarounds are storing the bound function on the instance or using an arrow-function class field. Passing the instance itself is a third, cheaper answer: `this` is already a stable reference, so add and remove sites cannot drift, and one object can serve several event types with a single registration each. ## Lookup happens at dispatch time The browser does not capture `handleEvent` when you register — it reads the property each time it dispatches. So an object can be registered before the method exists, and reassigning `handleEvent` later changes the behaviour of an already-registered listener. If the property is missing or not callable at dispatch time, the browser reports an error for that dispatch rather than failing the registration. Relying on this dynamism is clever but usually unhelpful; the value of knowing it is that a missing method surfaces at dispatch, not at `addEventListener`. ## When to use it It is a niche but legitimate tool. It reads well in vanilla Web Components and small class-based widgets, where one `handleEvent` switch keeps all of a component's event handling in one place and teardown is trivially correct. It reads badly in codebases full of standalone functions and arrow handlers, where a `switch (event.type)` block is just indirection. In an interview this is a differentiator rather than a screener: knowing it exists shows you have read the DOM specification rather than only the tutorials, and the `this`-binding contrast is the part worth being precise about.

  • How does `this` differ between a plain function listener and an object listener?
    In a plain (non-arrow) function listener `this` is the currentTarget — the element the listener was attached to. In `handleEvent` it is the listener object. Either way `event.currentTarget` gives you the element, so the object form loses nothing; it just points `this` at your own instance instead.
  • How does registering an instance as its own listener avoid the removeEventListener identity problem?
    Because the instance is a reference you already hold and never re-create, unlike `this.onClick.bind(this)`, which produces a new function on every call. Adding with `el.addEventListener('click', this)` and removing with `el.removeEventListener('click', this)` cannot drift apart.
  • Can you register the object before its handleEvent method exists?
    Yes. The browser reads the handleEvent property at dispatch time rather than at registration, so assigning it later works and reassigning it changes an already-registered listener's behaviour. If it is missing when an event arrives, that dispatch reports an error.

saying these in an interview costs you the question

  • Thinks any method name works, not just handleEvent
  • Says this inside handleEvent is the target element
  • Believes an object listener cannot be removed
  • Assumes the method is captured at registration time
  • Confuses handleEvent with the on* property handlers

context