Use Closures not Classes

November 5, 2018

I’ll agree that if you’re using React or any patterns that require inherence, then the class syntax makes sense (but then again, do you need inherence?) (Also see: Hooks – React)

I’ll also agree that TypeScript or Flow solve some of these issues.

However, if you don’t need inherence, classes are broken in a few key ways:

Global Scope

In a class, every method is public. While some have suggested prefixing an underscore on desired private method and or variable, it’s important to know that they are always public and someone always has a use case for using the “private” underscored method. See Hyrum’s Law.

class NavBar {
  constructor(props) {
    this.privateState = 'secretMessage'
  }
  _listStudents() {
    return [{ name: 'amy' }, { name: 'ben' }, { name: 'coconut' }]
  }

  fancyChildren() {
    return this._listStudents().map(student => student.fancy())
  }
}

const bar = new NavBar()
bar._listStudents()
// [{ name: 'amy'} ... ]

bar.privateState
// secretMessage

However, with a closure, you get true private methods:

const navBar = () => {
  const privateState = 'secretMessage'
  const listStudents = () => {
    return [{ name: 'amy' }, { name: 'ben' }, { name: 'coconut' }]
  }

  const fancyChildren = () => {
    return listStudents().map(student => student.fancy())
  }

  return {
    fancyChildren
  }
}

const bar = navBar()

bar.listStudents()
// Uncaught TypeError: bar.listStudents is not a function

bar.privateState
// undefined

Private methods give the freedom to decouple of implementation to the API of the object. With closures, APIs can be created by returning an object with the property names being the public API. By explicitly stating what you want your API to be, you gain much more flexibility to refactor at a later time without being tied to function names.

Private Class Fields

You may be thinking that the above isn’t applicable because of the private method proposal that’s, at the time of writing, is stage 3 (GitHub - tc39/proposal-private-methods). If you haven't read the proposal, the basics are that you can prepend a method name with # to make it private:

class Counter {
  #xValue = 0;

  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value;
  }

  #clicked() {
    this.#x++;
  }

  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }

  connectedCallback() { this.#render(); }

  #render() {
    this.textContent = this.#x.toString();
  }
}

const countee = new Counter()

countee.render()
// Uncaught TypeError: render is not a function

bar.privateState
// undefined

While this is a significant step forward, methods and fields defaulting to being public vs. private behavior is a small change, but in experience, have found it to lead to more exposed classes, losing some of the benefits of closures that default to private functions.

Getters / Setters

By far the worst thing about JavaScript classes is getters & setters. Getters and setters greatly limit the ability for developers to understand what code is doing by. For example, let's look at the following valid code:

const ourNewFish = new Fish()

ourNewFish.name = 'Mr. Bubbles'

console.log(ourNewFish.name)
// Sushi

With classes, we can use getters/setters to create proxies which take over the behavior of object properties. Getters & setters, like Proxy objects have their place, but in library development, not application code. Our Fish class above is defined as:

class Fish {
  get name() {
    return 'Sushi'
  }
}

By using getters and setters, we are now unable to determine if any side effects happen when ourNewFish.name is run. If any code uses getters & setters in a project, it requires assuming that all properties are getters & settings when debugging. You may be saying:

“What's the difference between that and ourNewFish.name()? they look the same”

While they look very similar, the context of what a line of code does is lost if getters and setters are used. In the case where you call a method, anyone reading the code knows that there is a method determining what is returned and it is not a simple value. It could be a function that returns a private name, or it could be a function that returns Sushi every time. With the function call, whoever is reading the code (or fixing a bug) know that there is more to the story. If a getter or setter is used, whoever is reading that code doesn’t know if there is more to the story, there is no way to tell. This creates code that is harder to read code & takes longer to debug. Next time just use a closer with get & set functions:

const Fish = () => {
  let name = 'Sushi'
  getName = () => name
  setName = newName => {
    name = newName
  }

  return {
    getName,
    setName
  }
}

this

The topic of this is highly debated in JavaScript, some people think that all developers should know how this works, others don’t. I tend to fall into the former category, but that's a talk for another essay. However, if you always set this to be lexical scope (like most programmers do), why even think about it? If you want dynamic scope and all the developers you work with understand it, then classes can be great. In my experience, however, most code doesn’t take advantage of it, and most developers don't understand it, which only leads it larger bug surface area. If I’ve found that rewriting code that uses dynamic scope is usually cleaner to the reader than the dynamic version. You can rewrite all code without using (or binding) this:

class ClickWatcher {
  constructor() {
    // don't forget to bind `this`
    this.doSomething = this.doSomething.bind(this)
    this.x = 'doing something'
  }
  doSomething() {
    console.log(this.x)
    //...
  }
  onClick() {
    window.addEventListener('click', this.doSomething)
  }
}

// vs

const ClickWatcher = () => {
  let x = 'doing something'
  const doSomething = () => {
    console.log(x)
    //...
  }
  const onClick = function() {
    window.addEventListener('click', doSomething)
  }
  return { onClick }
}

Closing

While classes give developers from other object-oriented languages a seemingly familiar tool, the pitfalls in JavaScript classes outweigh the benefits of familiarity, which are usually misunderstood especially in the case of this. I have yet to see a situation, that does not require inherence, where classes are better than closures for solving the problem at hand (If you find one, please email or tweet me). Next time you need to create a grouping of data inside an object, don’t use a class, use a closure.

If you liked this article and want to get updated when I write more, follow me on Twitter @719ben.