Classes in JavaScript received a ton of flak by the javascript community when they first came out, but for no good reason. They provide syntactical sugar around javascript’s prototypal inheritance features which are one of the most powerful aspects of language. They introduced no new functionality, but made more obscure parts of the language more explicit and approachable for those coming from other languages. I’m referring of course, to this and the new keyword.
function MyFunc() {}
const instance1 = MyFunc();
const instance2 = new MyFunc();
instance1 === instance2; // false
Execution context in javascript is everything, and this makes object oriented programming a bit tricky. Functions in javascript are first class citizens and can be used outside of the context of the class or instance they’re defined in.
const cat = {
name: 'foofoo',
speak() {
console.log(this.name);
},
};
const { speak } = cat;
cat.speak(); // foofoo
speak(); // undefined
Arrow functions were recently introduced as well where the this argument is bound to the enclosing scope.
class Cat {
constructor() {
this.name = 'foofoo';
this.speak = () => console.log(this.name);
}
}
If you’re unfamiliar with closures and classes in javascript, here’s a brief overview.
Closures in javascript are a method of capturing state by hiding them within javascript’s lexical scope and only exposing access through accessors (or not at all).
Closures are an integral part of functional programming, because large function chains are generated using them.
const closure = () ⇒ {
let counter = 0; // nothing can access foo directly except log + plus
function plus() {counter += 1;}
return {
log: () => console.log(foo),
plus,
};
}
const sum = a => b => {
return a + b;
}
sum(1)(1); // 2
For a more detailed overview of closures, see the w3schools overview which covers it pretty nicely.
ES6 Classes were introduced to facilitate the large influx of programmers coming to javascript from the java community who are more familiar with objected oriented programming where everything begins with class definitions. They expose a more concise way to work with objects, prototypes, and inheritance within javascript but leverage the same internal prototypal inheritance mechanisms under the hood.
function Logger() {}
Logger.prototype.log = function() {
console.log('wee');
};
const logger = new Logger();
logger.log();
// es6 class equivalent
class Logger {
public log() {
console.log('wee');
}
}
const logger = new Logger();
logger.log();
Many functional programming advocates are against uses classes and even go as far as to push for avoiding the use of the new and this keywords. There's even eslint rules banning their usage. This discourages the use of classes which I think is a huge mistake, especially when writing server side javascript. Here's why.
Closures allocate memory for each closure instance created, and the function bodies contained within (especially inline function declarations) need to be evaluated by the interpreter. This means both runtime interpretation of the code you write and space complexity of code are O(N), whereas when leveraging function prototypes the complexity is O(1). We can demonstrate the impact with some trivial examples on my local machine.
In this example we are storing a large string inside of a closure to demonstrate how these objects are taking up memory.
const mem3 = process.memoryUsage();
function MyObject() {}
MyObject.prototype.key = 'aksljhflkajshdflkajshfkjhsfslkjhlkjshd'.repeat(9999);
const instances = new Array(1000).fill('').map((x) => new MyObject());
const mem4 = process.memoryUsage();
console.log(`Memory Used: ${mem4.heapUsed - mem3.heapUsed} bytes`);
// Memory Used: 45104 bytes
In this example, we’re storing the large string on an object prototype (not that we’d ever actually do this) but it demonstrates that values stored on a function prototype are shared across instances.
For a more realistic example:
const mem1 = process.memoryUsage();
const getInstance = () => {
function foo() {
// Memory Used: 211680 bytes
}
function bar() {
// Memory Used: 283912 bytes
}
function baz() {
// Memory Used: 356144 bytes
}
function barry() {
// Memory Used: 428408 bytes
}
function bucky() {
// Memory Used: 500656 bytes
}
return {
log() {},
};
};
class Logger {
public foo() {}
public bar() {}
public baz() {}
public barry() {}
public bucky() {}
}
const getInstance2 = () => new Logger();
const instances = new Array(1000).fill('').map(getInstance); // Memory Used: 502640 bytes
const instances = new Array(1000).fill('').map(getInstance2); // Memory Used: 45696 bytes
const mem2 = process.memoryUsage();
console.log(`Memory Used: ${mem2.heapUsed - mem1.heapUsed} bytes`);
When leveraging closure chains to expose functionality, javascript apps take up dramatically more memory. This rarely matters for client-side applications (sizable chunk of the javascript community) but for server side development this can have a meaningful impact on the performance and scalability of your applications. CPU and memory cost time and money. By leveraging the prototypal features of the language you can improve the performance of application, improve your user experience, and save your company money.
So, should you go and refactor all of your code to use ES6 classes? Maybe not. But definitely worth considering in your design.
Thank you for reading! To be the first notified when I publish a new article, sign up for my mailing list!