Gbolahan Olagunju passionate about all things javascript.

How to decide between classes v. closures in JavaScript

3 min read 1107

JavaScript logo against an orange sky.

Before the arrival of ES6 classes in JavaScript, one of the fundamental ways to create a factory that produces similar types of objects was through closures and JavaScript constructor functions.

Closures and classes behave differently in JavaScript with a fundamental difference: closures support encapsulation, while JavaScript classes don’t support encapsulation.

NB: There is a proposal for this and it is in stage 3. It’s enabled in some browsers by default and can also be enabled through a Babel plugin.

Encapsulation is one of the core tenets of OOP (object oriented programming), and it’s essentially about protecting the private data of an object such that they can only be accessed or mutated via the public API exposed by the same object.

The public API makes sure that the private data of the object is accessed in a controlled way, and may decide to update private data provided that certain validation conditions are met.

Traditionally, JavaScript developers used _ to prefix the properties or methods that they intended to be private.

This s is problematic for a few reasons.

First off, new developers might not be aware of this and might modify private data.

Additionally, experienced developers might modify private data thinking they are sure of what they are doing, and this may cause unintended side effects.

Let’s consider an example that implements a user model first using classes (which are synthetical sugar for constructor functions), and then do the same with a closure.

We made a custom demo for .
No really. Click here to check it out.

Note the difference:

// class Example
 class UserClasses {
  constructor({firstName, lastName, age, occupation}){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.occupation = occupation;
  }
  describeSelf() {
    console.log(`My name is ${this.firstName} ${this.lastName}, I am ${this.age}years     Old and i work as a ${this.occupation}`);
  }
getAge() {
    return this.age;
  }
}
const gbolahan = new UserClasses({firstName: "Gbolahan", lastName: "Olagunju", age: 28, occupation: "Software Developer"});
gbolahan.describeSelf();
//My name is Gbolahan Olagunju. I am 28 years old and I work as a Software Developer.
// closure Example
const UserClosure = ({firstName, lastName, age, occupation }) => {
  return ({
    describeSelf : () => {
      console.log(`My name is ${firstName} ${lastName}, I am ${age}years Old and i work as a ${occupation}`);
    },
    getAge: () => age;
  })
}
const zainab = UserClosure({firstName: "Zaynab", lastName: "Olagunju", age: 30, occupation: "Economist"});
zainab.describeSelf();

//My name is Zaynab Olagunju. I am 30 years Old and I work as a Economist.

From the above example, you’ll notice that we can implement an object blueprint using either closures or classes. However, there are a few differences that are important for us to identify.

The classe model uses the this keyword to refer to private data, while we aren’t referring to this in any way in the closure implementation. For this reason, closures are preferable as this in JavaScript doesn’t always work as expected when compared to other traditional OOP languages.

The class implementation uses the new keyword to create an instance, while we simply just call the function in the closure implementation.

The closure implementation supports encapsulation, since we don’t directly have access to its private data except through the methods it exposes. We can manipulate the private data of the class implementation, thus making the class implementation more brittle.

On the other hand, classes can be faster.

Consider this example:

const zainab = UserClosure({firstName: "Zaynab", lastName: "Olagunju", age: 30, occupation: "Economist"});

console.log(zainab.firstName) // undefined
//can only be accessed via the expose API
console.log(zainab.getAge()) // 30
vs
const gbolahan = new UserClasses({firstName: "Gbolahan", lastName: "Olagunju", age: 28, occupation: "Software Developer"});

console.log(gbolahan.firstName) // Gbolahan

Here, the class implementation tends to be faster because of how it is implemented internally by the browser or Node environment.

Every instance of the class shares the same prototype, meaning that a change in the prototype will also affect every instance. Meanwhile, every instance created by the closure implementation is unique.

Let’s see how this plays out visually:

Class implementation.

From the diagram above, we can roughly imagine that the class implementation creates one blueprint in memory that all instances created through it will share.

On the other hand, the closure implementation creates a fresh reference in memory for every instance, thus making it less memory efficient.

Let’s implement this in Node and see the values that this logs out using process.memoryUsage():

// class Example
class UserClass {
  constructor({firstName, lastName, age, occupation}){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.occupation = occupation;
  }
  describeSelf() {
    console.log(`My name is ${this.firstName} ${this.lastName}, I am ${this.age}years Old and i work as a ${this.occupation}`);
  }
  getAge() {
    return this.age;
  }
  showStrength () {
    let howOld = this.age;
    let output = 'I am';
    while (howOld-- > 0) {
      output += ' very';
    }
    return output + ' Strong';
  }
}
const individuals = [];
for (let i = 0; i < 4000; i++) {
    const person = new UserClass({firstName: "Zaynab", lastName: "Olagunju", age: [i], occupation: "Economist"})
    individuals.push(person)
  }
  const used = process.memoryUsage();
for (let key in used) {
  console.log(`${key} ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
}
const start = Date.now()
individuals.map(person => person.showStrength());
console.log('Finished displaying strength in ' + (Date.now() - start) / 1000 + ' seconds');

//This was the result that was displayed by my mac
// rss 29.72 MB heapTotal 17.73 MB heapUsed 6.99 MB external 0.01 MB
// Finished displaying strength in 1.233 seconds

Now let’s compare this to the closure implementation:

const UserClosure = ({firstName, lastName, age, occupation }) => {
  return ({
    describeSelf : () => {
      console.log(`My name is ${firstName} ${lastName}, I am ${age}years Old and i work as a ${occupation}`);
    },
    getAge: () => {
      return age;
    },
    showStrength: () => {
      let howOld = age;
      let output = 'I am';
      while (howOld-- > 0) {
        output += ' very';
      }
      return output + ' Strong';
    }
  })
}
const individuals = [];
for (let i = 0; i < 4000; i++) {
    const person = UserClosure({firstName: "Zaynab", lastName: "Olagunju", age: [i], occupation: "Economist"})
    individuals.push(person)
  }
  const used = process.memoryUsage();
for (let key in used) {
  console.log(`${key} ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
}
const start = Date.now()
individuals.map(person => person.showStrength());
console.log('Finished displaying strength in ' + (Date.now() - start) / 1000 + ' seconds')
// rss 30.12 MB heapTotal 18.23 MB heapUsed 8.03 MB external 0.01 MB
// Finished displaying strength in 4.037 seconds

NB: using process.memoryUsage() is not the most accurate way to determine memory usage, as it varies slightly on different runs. Still, it gets the job done.

Conclusion

Closures offer simplicity, since we don’t have to worry about the context that this is referring to.

Meanwhile, classes tend to be slightly more performant if we are going to be creating multiple instances of an object.

If we are creating multiple instances of an object, classes will best suit our needs. Meanwhile, if we don’t plan to create multiple instances, the simplicity of closures may be a better fit for our project.

The needs of the project will determine whether closures or classes are most appropriate.

Happy coding!

: Debug JavaScript errors easier by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Gbolahan Olagunju passionate about all things javascript.

Leave a Reply