Update

I’m very humbled by the insightful discussion on HackerNews. Really, thank you! I have now updated the blog post to address the comments. You can always view the diff on GitHub.

Introduction

As much as I dislike the class syntax in JavaScript, it used to be my default choice when I needed an object factory, with support for static and instance level properties.

I recently found a clean way to eliminate the class syntax while still maintaining these properties, and in this post, I’ll show you how.

Classy issues

Classes are plagued with issues, such as:

this is awkward

Having to write:

this.progressBar.addEventListener(this.handler.bind(this));

is much worse compared to:

progressBar.addEventListener(handler);

The this prefix is mandatory for every instance property, which increases code bloat. When passing methods around, you have to carefully rebind them to the correct object.

Regarding private properties

I initially wrote that classes don’t support private properties. That was completely wrong, as this MDN article mentions. Using private properties helps better encapsulate internal logic, that we don’t want external code to depend on. I’m glad JS classes support that.

Regarding readonly properties

I initially wrote that we cannot have instance properties that are public but readonly. For example, this is valid JavaScript:

class MyClass {
  static prop = 123;
}
MyClass.prop = 456;

As comments pointed out, you can define a getter instead:

class MyClass {
  static get prop() {
    return 123;
  }
}
MyClass.prop = 456; // doesn't change the value

Poor bundler optimization

If you are building a large class encapsulating some complex logic, you likely have dozens of private methods as “helper” methods, but only a few externally exposed methods. Unfortunately, build tools like terser or webpack cannot optimize them well. For example:

class MyClass {
  constructor() {
    this.prop = 5;
  }
  publicMethod() {
    this.property = 6;
  }
}
const unused = 5;
const instance = new MyClass();
console.log(instance.prop);

When optimized with npx webpack --mode production, this results in:

(()=>{const o=new class{constructor(){this.prop=5}publicMethod(){this.property=6}};console.log(o.prop)})();

Notice how: 1. the unused instance method is not removed, and 2. the name of this instance method is not simplified.

Poor linting experience

Unused methods and properties are never flagged by eslint (here’s an example class). This makes it difficult to refactor existing code.

// flagged
const unusedVar = 123;

class MyClass {
  // not flagged
  static unusedStaticProperty = 6;

  constructor() {
    // not flagged
    this.unusedProperty = 5;
  }
  
  // not flagged
  unusedMethod() {
    
  }
}

new MyClass();

Not hoisted up

As JavaScript developers, we expect functions (that are blocks of code) to be hoisted up in their own lexical scope. Sadly, classes do not share this same property, which leads to workarounds like these:

var MyClass = class MyClassInternal {
  // ...
};

This is not an issue when using module-oriented development. But if you are not using modules, then this becomes a hassle.

Example

Here is an example class that we can rewrite:

class Dog {
  static AVERAGE_HEIGHT_FT = 4;
  static AVERAGE_WEIGHT_KG = 100;
  static _PRIVATE_MAGIC_HEIGHT = 3.14;

  constructor(height, weight) {
    this.height = height;
    this.weight = weight;
    if (this.height === this._PRIVATE_MAGIC_HEIGHT) {
      this._privateMakeTaller();
    }
  }

  _privateMakeTaller() {
    this.height = Dog.AVERAGE_HEIGHT_FT + 1;
  }

  printHeight() {
    const status = this.height > Dog.AVERAGE_HEIGHT_FT ? 'taller' : 'not taller';
    console.log(`Your dog is ${this.height} ft tall. The dog is ${status} than the average height`);
  }

  static getAverageHeight() {
    console.log(`Average height for dogs is ${Dog.AVERAGE_HEIGHT_FT} ft`);
  }
}

This code has a mix of static and instance-level methods and properties. Let us assume we want to expose only three methods: constructor, printHeight and getAverageHeight. Notice the weight properties are unused, which we might automatically detect in our re-written version. We also have a secret method and a secret property, that we want to encapsulate well.

Closures to the rescue!

JavaScript closures are amazing. We can use them to emulate static properties, instance properties, private properties, as well as readonly properties. Here’s the rewritten version of the above class:

const Dog = (function createDogClass() {
  const AVERAGE_HEIGHT_FT = 4;
  const AVERAGE_WEIGHT_KG = 100;
  const _PRIVATE_MAGIC_HEIGHT = 3.14;

  function init(heightInput, weightInput) {
    let height = heightInput;
    const weight = weightInput;
    if (height === _PRIVATE_MAGIC_HEIGHT) {
      _privateMakeTaller();
    }

    function _privateMakeTaller() {
      height = AVERAGE_HEIGHT_FT + 1;
    }

    function printHeight() {
      const status = height > AVERAGE_HEIGHT_FT ? 'taller' : 'not taller';
      console.log(`Your dog is ${height} ft tall. The dog is ${status} than the average height`);
    }

    return {
      printHeight,
    };
  }

  function getAverageHeight() {
    console.log(`Average height for dogs is ${AVERAGE_HEIGHT_FT} kg`);
  }

  return {
    init,
    getAverageHeight,
  };
})();

Immediately, we see several advantages:

  1. Unused properties are flagged by eslint (see example)
  2. Webpack optimizes our code with variable renaming and dead code removal.
  3. We have lesser code bloat thanks to removing this. and Dog. prefixes.
  4. Our private and readonly properties are truly private and readonly now.

    As we discussed earlier, regular classes also support this.

How does this work?

Static scope

The scope inside createDogClass is the static scope. Variables and functions declared in this scope are shared by all instances. From this scope, we return an object of properties that are exposed externally. In this case, we return:

  1. init which acts as our constructor, and
  2. getAverageHeight which lets the users know the average height of dogs.

Notice how:

  1. the magic height property is private, and
  2. the average height property is public (exposed via getAverageHeight) but readonly

Instance-level scope

We replaced the constructor with an init function that does the same job. Now, to create an instance, we call Dog.init(...) (instead of new Dog(...)). Note that each invocation of init returns a new object instance, which also comes up with a separate lexical scope (very handy for us!)

From inside init, we return all publicly exposed properties. In this case, we only expose printHeight. Notice how:

  1. _privateMakeTaller is a private method, and unique for every instance of Dog
  2. the static properties from Dog scope are easily accessible.
  3. height and weight values are not shared across instances (unique per instance)

Conclusion

I hope this post helped you understand how to rewrite JavaScript class syntax into closures.

Post conclusion

Motivated by the HN comments, here’s some additional considerations when using closure syntax to emulate classes:

  1. Developer experience takes a hit because now the instanceof check no longer works. Further, the syntax Dog.init might be strange to use instead of the familiar new Dog
  2. Every new object ships with a copy of all the methods, which is bad for memory optimization of the program.

Also, I don’t recommend that we start switching all classes to closures straightaway. With this blog post, I want to highlight the difference between the class pattern vs the closure pattern, and how each of them have their differences and advantages. The choice of which one to use is ultimately yours to make.

Further discussion

Mika Genic reached out to me via e-mail to provide a clever design that clearly defines a public interface while also avoiding an IIFE. Here’s an example:

function Dog(name) {
  // define public interface
  const self = Object.assign(this, {
    publicFn1,
  })

  // init private state
  let created = new Date()
 
  // call constructor
  constructor()

  return self
 
  // logic
  function constructor() { console.log(`dog created`) }
  function publicFn1() { console.log(`${privateFn1()}`) }
  function privateFn1() { return `${name} ${created}` }
}

let dog = new Dog(`Billy`)
dog.publicFn1()

Note that:

  1. there is no IIFE, which makes understanding the code far simpler.
  2. the public interface is clearly separated from the rest of the logic
  3. You can use the traditional new keyword to construct objects from this class.
  4. You can avoid this entirely.

Some readers will notice that this is very similar to the pre-ES6 way of declaring classes. Here’s an example post on StackOverflow.