Javascript: About Classes, Binds & Arrows

Vaibhav Nayak
The Startup
Published in
8 min readAug 16, 2019
A Typical React Component

When I first started working with JavaScript at my workplace, I found some of the nuances of the language a bit tricky to grasp. It was only after spending several hours online reading (mostly MDN, and I’ll recommend going there instead of reading this if you have the time) and experimenting that I was able to build the right mental models of how things work.

After reading this article, I hope that you get a sense of the answers to the following questions:

  • What’s a constructor function?
  • What’s the difference between a function prototype and an ES6 class?
  • What’s ‘this’?
  • Why bind?
  • What’s the class field syntax?
  • Why should one be careful while using arrow functions as class fields?

ES6 Classes & Constructor Functions

ES6 classes are great to use, especially for those coming from an OOP (Read ‘Java’) background. Things get a little messy though, when you start using them in frameworks such as React — for example, you suddenly have to ‘bind’ some methods in the constructor without which they don’t work as you’d want them to. To understand why that is, it helps to first figure out some implementation details.

ES6 Classes Are Syntactic Sugar

JavaScript has been supporting OOP principles even before ES6 classes. We have:

  • Something called a ‘Constructor Function’
  • A parent’s prototype can be ‘inherited’
  • Methods can be defined on the ‘prototype’ of the function
  • Instances can be instantiated using the ‘new’ keyword

Explaining the prototype model in detail deserves a story of it’s own, but finding resources on it should be pretty straightforward.

With ES6 classes, nothing much changes, except the syntax. In fact, an ES6 class can be equivalently written using a constructor function, and the same goes the other way round!

Spidey pointing at Spidey meme, with labels ‘prototype model’ and ‘es6 classes’ respectively
ES6 class syntax is just syntactic sugar

The usage of a class is agnostic of how it was defined. The following code snippet should make things clear:

Note: In the snippet above, I say ‘Almost equivalent prototype’, as ES6 classes have some additional checks in place to protect against incorrect use. These checks can be added to the constructor function syntax too, but it makes the code verbose and aren’t necessary here to drive home the point being made.

Shared Methods

An interesting consequence of the prototype model (ES6 classes internally use the same) is that methods defined on the prototype (Or class methods in an ES6 class) are shared by all the instances of the same class. Put differently, there exists only a single instance of the function, and each class instance calls the same function whenever the corresponding method is called on the class instance. This is unlike most other OOP based programming languages, where each instance gets its own set of methods. Consider this snippet:

The Execution Context: What’s this?

Given that the class methods are shared, the JavaScript engine needs to know which specific instance is calling the function, so that it can execute the method in the right context. It does so using a special keyword — this. For a constructor function, this refers to the instance being created by the constructor. But for any other function, this in a function’s body refers to the object used to call the function (Unless the function is an arrow function, more on it below). So for example, when redRanger.flyToTheRescue() is called, the this used to get the name property of the instance in the function definition refers to the redRanger instance object itself, due to which this.name resolves to Red Ranger , as expected. Similarly, when blueRanger.flyToTheRescue() is called, this in the function definition refers to the blueRanger object.

The concept of execution context however is not restricted to classes: In fact, the same principles apply when functions are assigned to regular object properties and called. Consider this (LOL):

The Bind

Methods work as expected when we call them using objects, thus providing an execution context to them. What happens though, when a function using this within it’s body is called without an explicit execution context?

Global Execution Context

The answer to the question above is a bit nuanced. If strict mode is enabled, a TypeError is thrown — TypeError: this is undefined . Otherwise, the global execution context is used as the execution context for the function — in browsers it is generally the window object. By default, classes defined using the ES6 class syntax already have strict mode enabled, therefore the methods defined on them throw the error when called without the execution context.

Binding The Execution Context

Irrespective of whether strict mode is enabled, the behavior is generally undesirable. Often, there are use cases where a function’s reference has to be passed, and it is expected that the function will be called without an explicit execution context. When called, we would still want the function to use the right context. Some examples are event listeners, click handlers, etc. In such use cases, we bind a function to an execution context, using the Function.prototype.bind method. The bind method returns a new function whose execution context is bound to a specific reference, irrespective of the provided execution context. Code is almost always easier to read:

Arrow Functions

Arrow functions are almost identical to regular functions. Other than the obvious difference in syntax, there is another crucial distinction — unlike regular functions, arrow functions have automatically bound execution context. The context that the arrow function is bound to is the same as the context that this refers to in the lexical scope in which the arrow function is created — now that’s a mouthful. Code:

Before arrow functions were introduced, we would capture the execution context into a local variable and use that variable in a regular function in place of this . As the function would close over the local variable, it would always have access to the right execution context. Arrow functions are poly-filled similarly, too! For example, the getValueArrow function could be rewritten using a regular function, with no change in behavior:

Arrow Functions As Class Fields

There is proposal to support a new syntax called ‘class fields’, which among other things, will allow us to initialize instance properties without having to do it in the constructor. This syntax is already quite popular though, and can be used as such:

The above class is equivalent to having the initialization done in the constructor. Note that as the functions incrementCount and handlePress are defined as class fields, they too will be assigned in the constructor rather than being a part of the prototype. Here’s the equivalent class definition:

As can be seen, the handlePress function reference is being passed to the onClick event handler of the button component. We don’t have to worry about execution context though, as handlePress is defined as an arrow function in the constructor and as established earlier, it uses the same execution context as available in the lexical scope.

The Problem With Unchecked Use Of Arrow Functions As Fields

In the same React component example above, consider the incrementCount function. It’s reference is not passed to any other scope, it is always called using the component instance as the execution context. It’s the perfect candidate to be a method defined on the prototype and be shared among all the component instances of the class! It however is not, and instead, a new function is created for every PlainOldCounter component. Ideally, the component should have been defined like this:

This would create only a single instance of the incrementCount method, shared by multiple PlainOldCounter components.

Performance Implications

I performed a rudimentary benchmarking test using two classes — one used an unnecessary arrow function defined as a field, akin to incrementCount , the other used a class method that performed the same function. This was done on a 2015 MacBook Pro Edition, 16GB RAM, in Firefox. I used the following function to test and ran it on JSBench.me:

You can find the test here.

It turns out, for creating 1000 instances of the class and calling the getValue method on each of them, the arrow field class is ~60% slower than the one with the class method! If you also factor in the potential increase in memory footprint due to the unnecessary duplicate methods, arrow functions as fields should then be a strict no, right?

WRONG.

Relative Performance By Itself Is Meaningless

It’s important that the test conducted is as close to real conditions as possible. The base absolute performance is also an equally important factor. Consider my own example: Firstly, it isn’t very often that one creates 1000 instances of a class all at once. Secondly, here’s a fun number: for the class with the class method, the perfTest function returns ~0.04 — implying that it takes about 40 microseconds to allocate memory for 1000 instances of the class and call a method on each of them. MICROSECONDS! When your product manager wants you to display a pop-up during the 4th minute of a user’s 2nd session on the 3rd consecutive day of user interaction IF the user has not already been displayed a pop-up in the last 13 days, optimizing at the scale of microseconds should be the least of your concerns. Yes, maybe you’d want to work it out in case you’re running continuous, complex animations or maybe a super-high RPM server running on a single machine (Why?), but for almost every other case, you don’t really need to bother about performance at this scale.

Then what’s my argument against it?

I have two: Language semantics, and not misguiding junior developers.

A Case Against Unnecessary Arrow Functions As Fields

The language semantics argument is fairly straight forward — whenever we use a programming language, I personally think that it helps to write code idiomatically. Every line of code has implications, and as a user of the language, I think we need to be aware of them, even if sometimes, it’s only a high level understanding. Making arguments on performance, code maintenance, and so on is meaningless without this fundamental understanding on each of these. It’s okay to pick one out of several options, but it helps to be aware of the trade-offs involved. Without this awareness, any argument is meaningless.

The junior developer argument is a more personal one —when I first started out right after college at my workplace, I was asked to build a feature in a codebase that was sprinkled with code similar to unnecessary arrow functions here and there. The phrase ‘tech debt’ was alien to me, and I assumed that all the code that I see in the codebase is well-written, perfect code. So of course, I spent less time learning and more time referring the codebase to build the feature under tight time constraints, and of course, my first code was an addition to the already existing mess. Luckily though, I had great colleagues to work with and there was the Internet; I was more than happy to remove all the remnants of the feature code when it got deprecated. So my argument then is this — junior developers are inadvertently going to refer the existing code as they get started, and poorly thought out code is only going to slow them down. When possible, can we be more careful?

While I started this story as an attempt to explain why unnecessary arrow functions in ES6 classes should be avoided, I realized that it would be of very little help unless I also explain some of the other core fundamentals. Anyway, do let me know if this helped you understand JavaScript better, or if you think I’ve got it all wrong.

Cheers!

--

--