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.
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:
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.
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!
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
3 Replies to "How to decide between classes v. closures in JavaScript"
Why aren’t you building your Closure like your class and getting the best of both worlds?
let UserClosure = function(firstName, lastName, age, occupation) {
this.firstName = params.firstName;
this.lastName = params.lastName;
this.age = age;
this.occupation = occupation;
let privateValue = “Can’t see this!”;
function privateFunction(args) { // private method }
}
UserClosure.prototype.getAge = function() { return this.age; }
UserClosure.prototype.describeSelf = function() { …. };
let someOne = new UserClose(“first”, “last”, 55, “dev”);
This isn’t intended as argumentative. I’m looking for why I should start using classes instead of the above construction in some upcoming work.
This is not a closure but a constructor function. You have it all mixed up badly :/
Here’s how to get the best of both worlds.
const Foo = (function() {
//create a prototype.
const prot = {
bar(bas) {
bas = bas || this.fallbackBas;
console.log(“bar says ” + bas);
}
} //end of prot.
//constructor.
return function(fallback) {
const o = Object.create(prot);
//new object, prot as prototype.
o.fallbackBas = fallback;
return o;
} //constructor
})(); //iif
const f = new Foo(“This is a fallback.”);
f.bar(“This is not a fallback.”);
f.bar();
/*Output:
bar says This is not a fallback.
bar says This is a fallback.
*/
All the funcs are created only once, and other vars can go in the same outer func.