Function identity in JavaScript, or how to remove event listeners properly
Tags: javascript, beginners, webdev
Published: 2018-06-09 23:58:25

---
title: Function identity in JavaScript, or how to remove event listeners properly
published: true
description: One of the many possible sources of confusion for JavaScript beginners is the DOM’s event listener API.
tags: javascript,beginners,webdev
---

Last week I looked over some JavaScript a colleague of mine had written, and I noticed an interesting mistake. Interesting because I knew that I had myself tripped over this at some point, and seeing it in someone else’s code showed me that it’s indeed a tricky problem more people are struggling with, so I thought it might be worth sharing.

(Also I wrote a very lengthy issue for them, describing the problem and how to fix it, which was the basis for this post.)

Here’s the first part of the code in question (we’re talking about plain ES6 in the browser, no frameworks. Also this is not the original code, I removed everything unrelated to the point I’m trying to make):

Ⓐ

```js
// For completeness, imagine these being something sensible:
let elements = document.querySelectorAll(/* ... */)
function mouseenterHandler(element) {
  // ...
}

// This is the interesting part:
elements.forEach(element => {
  element.addEventListener('mouseenter', () => {
    mouseenterHandler(element)
  })
})
```

Some DOM elements are queried for and event listeners are being attached to each.

And then, somewhere further down, in a teardown routine:

Ⓑ

```js
elements.forEach(element => {
  element.removeEventListener('mouseenter', () => {
    mouseenterHandler(element)
  })
})
```

This, apparently, is an attempt to unregister the event listeners by calling `removeEventListener` with *equivalent anonymous functions* as arguments.

## The problem

`() => { this.mouseenterHandler(element) }` in Ⓐ is an *anonymous function* that we retain no reference to (i.e., it is not saved in a variable or given a name in any way).

`() => { this.mouseenterHandler(element) }` in Ⓑ is an *equivalent anonymous function*. The important thing to note here is that they’re *equivalent*, but not *the same*.

## How JavaScript compares functions

Functions in JavaScript are objects, and, like all other objects, they’re compared by reference. What that means is that JavaScript has no way to determine the equivalence of two functions.

### Y u no serialise them?

Now we might think, JavaScript can already serialise functions, why doesn’t it simply compare them by their string representation?

```js
let f1 = (x) => { return x + 1 }
let f2 = (x) => { return x + 1 }

console.log(f1.toString()) // '(x) => { return x + 1 }'
console.log(f2.toString()) // '(x) => { return x + 1 }'

// ... so:
console.log(f1.toString() === f2.toString()) // true - yay!?
```

But let’s consider this slightly different, although arguably equivalent function:

```js
function f3(x) {
  return x + 1
}

console.log(f3.toString()) // 'function f3(x) {\n  return x + 1\n}'
```

It is quite obvious that `f1.toString() === f3.toString()` and `f2.toString() === f3.toString()` will be false, even though it’s also trivial that `f1(x) === f3(x)` and `f2(x) === f3(x)` will be true for any given x in \[`Number.MIN_SAFE_INTEGER`, `Number.MAX_SAFE_INTEGER - 1`\] (and actually for many other values of `x`, too).

So this method would only work for functions that are written out in exactly the same way.

### How it’s actually done

In JavaScript, there are essentially three\* basic data types that are *immutable*, which is a fancy comp-sci way of saying they sort of behave like things do in pen-and-paper mathematics. One of them is the `Number` type. In maths, there is only one number 𝟐. It makes no sense to talk about this 𝟐 here and that 𝟐 over there. We can write out the *character* «𝟐» as often as we’d like to, but every one of them will still be a reference to the same single number 𝟐. It works the same way in JavaScript:

```js
let a = 2
let b = 2

console.log(a === b) // true
```

The other two\* basic data types in JS are `String` and `Boolean`. That’s why we were able to compare the string representations of `f1`, `f2` and `f3` by equivalence.

**Everything else** in JavaScript is compared *by reference*. Everytime we write `[]`, we create *a new array*, which is *not the same one* as the next time we write `[]`, everytime we write `{}` we create a new object, and everytime we write `() => {}`, we create a new function.

<small>(Strictly speaking, it’s not every time *we write* `[]`, `{}`, or `() => {}`, it’s every time one of these *is evaluated*. Which is actually a big difference. Imagine a function like `function makeObj () { return {} }`—everytime we’d call `makeObj()`, it’d return a fresh new object.)</small>

In other words,

```js
console.log([] === []) // false
console.log({} === {}) // false
console.log((() => {}) === (() => {})) // false, too!
```

but

```js
let a1 = []
let a2 = a1
console.log(a2 === a1) // true
let o1 = {}
let o2 = o1
console.log(o2 === o1) // true
let f4 = () => {}
let f5 = f4
console.log(f5 === f4) // also true
```

## What’s it got to do with our eventListeners

Imagine that for each element in the DOM, JavaScript creates an array in which to keep all the mouseenter listeners, like this:

```js
let myElementMouseenterListeners = []
```

And every time we add an event listener, like

```js
myElement.addEventListener('mouseenter', () => { console.log('yay') })
```

JavaScript internally just adds it to the array:

```js
let myListenerToAdd = () => { console.log('yay') }

myElementMouseenterListeners.push(myListenerToAdd)

console.log(myElementMouseenterListeners) // [ [Function] ]
```

When the `'mouseenter'` event occurs, JS will call every function in the array:

```js
let myMouseenterEvent = new MouseEvent('mouseenter')

myElementMouseenterListeners.forEach(mouseenterListener => {
  mouseenterListener(myMouseenterEvent)
})
```

And when we try to remove an event listener, JavaScript will iterate over the array of event listeners, comparing each function in there to the one we are trying to remove, and if it happens to be *the same*, remove it from the array.

Imagine we do:

```js
myElement.removeEventListener('mouseenter', () => { console.log('yay') })
```

And JavaScript does:

```js
let myListenerToRemove = () => { console.log('yay') }

for (let i = 0; i < myElementMouseenterListeners.length; i++) {
  if (myElementMouseenterListeners[i] === myListenerToRemove) {
    myElementMouseenterListeners.splice(i, 1)
    break
  }
}

console.log(myElementMouseenterListeners) // still [ [Function] ]
```

That means, when the loop arrives at the listener we added in the beginning, it will compare it to the one we supplied to `removeEventListener`, so what happens is basically this:

```js
() => { console.log('yay') } === () => { console.log('yay') }
```

Which, as we examined before, evaluates to false.

What this means is that code like

```js
element.removeEventListener('eventname', () => { console.log('event handled') })
```

calling `removeEventListener` with an *anonymous function* that is just newly created in that very moment as second argument, **can never have any effect**. It will, instead, fail silently.

## What we need to do instead (possible solutions)

For `removeEventListener` to have any effect, we’ll have to supply a reference to a function that we actually registered before via `addEventListener`.

Generally, something like

```js
let element = document.querySelector(/* ... */)
function mouseenterHandler() {
  // ...
}

element.addEventListener('mouseenter', mouseenterHandler)

element.removeEventListener('mouseenter', mouseenterHandler)
```

will work, because we’re using a reference to the same function everywhere, so when `removeEventListener` is called, it will find out which function to remove by comparing it like `this.mouseenterHandler === this.mouseenterHandler`, which we can see is trivially true.

Now the “problem” is that our actual `mouseenterHandler` is generalised - it takes an element as a parameter. Which is certainly a better solution than writing a new `mouseenterHandler` function for each element we’re going to work with! But now we have to get the parameter in there somehow, and wrapping the call to `mouseenterHandler` in an anonymous function won’t work here, as I verbously showed above.

### Solution 1: create specialised versions of the event handlers for each element

We could create specialised versions of `mouseenterHandler` that already have the extra argument baked in right after we populate `elements`. Something like:

```js
let elements = document.querySelectorAll(/* ... */)
let enhancedElements = []

elements.forEach(element => {
  enhancedElements.push({
    element,
    mouseenterHandler() { mouseenterHandler(element) },
  })
}
```

And then, change the code adding the handlers into

Ⓐ

```js
enhancedElements.forEach(ee => {
  ee.element.addEventListener('mouseenter', ee.mouseenterHandler)
})
```

and the removal, respectively, into

Ⓑ

```js
enhancedElements.forEach(ee => {
  ee.element.removeEventListener('mouseenter', ee.mouseenterHandler)
})
```

This will work, but it will also create an extra object and function per element, which may not be a problem if there aren’t that many elements, but still, there’s a more elegant way …

### Solution 2: change our event handlers to work with the arguments they already get

The browser will call our event handlers with the event as the first argument. And an event is just an object with a number of properties, one of them being `event.target`, which is a reference to the element on which the event occurred. So, why not change our event handlers to use that, so we don’t have to shoehorn the element in there manually?

If our mouseenterHandler, for example, looked like this:

```js
mouseenterHandler(element) {
  element.classList.add(/* ... */)
}
```

We could just change it to use `event.target` instead:

```js
mouseenterHandler(event) {
  event.target.classList.add(/* ... */)
}
```

Or use destructuring right in the parameter list, so we don’t have to repeat the `event.` part:

```js

mouseenterHandler({ target }) {
  target.classList.add(/* ... */)
}
```

With this solution, we can leave the `let elements = document.querySelectorAll(/* ... */)` line how it is; no extra objects or functions needed, we’ll just have to change Ⓐ into:

```js
elements.forEach(element => {
  element.addEventListener('mouseenter', mouseenterHandler)
})
```

And Ⓑ, accordingly:

```js
elements.forEach(element => {
  element.removeEventListener('mouseenter', mouseenterHandler)
})
```

Our event handlers are now “universal” and can be used unchanged with any element.

<small>* I lied. `undefined` is a type, too.</small>

---

Thank you for reading! This is my first post here on dev.to, and also I’m not a native English speaker, so general feedback regarding style, helpfulness, etc. would be appreciated :)