In this article, we'll learn about JavaScript class methods and why we shouldn't use arrow functions in methods definition.
Classes have been introduced to JavaScript as part of ES6. Before that, for create a class we used a function where we were describing a JavaScript object with logic in methods and properties using this
notation.
Let’s create a simple class with some methods:
class Pet {
name = 'dog';
getName() {
return this.name;
}
}
There is only one property name
and one method getName
in this Pet
class. Method definition has been set to default function declaration.
Classes can be created this way, but it wasn't possible in ES5 before. Let’s see how we could create the same class in ES5:
"use strict";
var Pet = (function () {
function Pet() {
this.name = 'dog';
}
Pet.prototype.getName = function () {
return this.name;
};
return Pet;
}());
Here is how class looks like in ES5 - we’ve create function with property name
equals dog
. We used this
notation to refer to object we going to create. In case when we want to create method we could put it as property to object, but would be much better to add the method to the prototype of our function and we actually did it. It allows us to use getName
function like it right inside the object and Inheritance and the Prototype chain helps us here:
const pet = new Pet();
const pet2 = new Pet();
console.log(Pet.prototype.getName === pet.getName); // true
console.log(pet.getName === pet2.getName); // true
As you can see we can get access to the getName
method right from instance of the our Pet
class. One more important thing is that method created only once. Even if we create 1000 instances of the Pet
class this function will be reused in every instance.
The class syntax is simple and it also put all method to prototype automatically. We don’t need to think about as before. But together with classes in ES6 we got arrow functions, the new way how to define and work with classes:
const getDate = () => new Date()
The syntax of arrow functions is cleaner and shorter. They also have different behavior on execution level - they have bound context from definition scope instead of execution scope in default function, which prevents developers from making a common mistake by passing a function as an argument to another method or function, which could lose the context.
With the advent of the popularity of arrow functions, they began to be used everywhere and even as class methods. Let’s add new method to our class:
class Pet {
name = 'dog';
getName() {
return this.name;
}
getNameArrow = () => this.name;
}
We’ve defined the getNameArrow
arrow function. It’s do completely the same thing and looks even better and cleaner by the way. But what could be wrong here?
First thing you might noticed we use =
sign to define a method, which actually means we were define a property. Even VScode highlighted it for us:
As we remember in ES5 vs ES6 syntax - all properties will be added right to the class instance instead of prototype. Let’s try to proof it. We can pass our code of class via Babel or TypeScript compiler, even online version, and see result:
"use strict";
var Pet = /** @class */ (function () {
function Pet() {
this.name = 'dog';
this.getNameArrow = function () { return this.name; };
}
Pet.prototype.getName = function () {
return this.name;
};
return Pet;
}());
Here we can notice our methods defined with different ways. The getName method still placed in prototype, but getNameArrow has defined as property inside of object and it will be created for every single instance. Let’s check it in real case:
const pet = new Pet();
const pet2 = new Pet();
console.log(pet.getNameArrow === pet2.getNameArrow); // false
Every time when we create new instance, we create new function getNameArrow
as well. Just imagine the case when we need 1000 instances of our Pet with 20 methods inside created as arrow function. It means will be created 20000 useless functions. And this is just when we use arrow functions for that.
This is actually root cause why we should avoid using arrow functions in classes, or at least think and understand when we can do it. If you know different cases or disagree with that, please put your thoughts to comments.
Top comments (3)
Kudos for pointing this out, this really matters when things start to scale. :)
Thank you, Sebastian!
I agree, that we should avoid them due to performance questions, but nevertheless, the arrow functions are still a must for callbacks. I cannot remember how many times I have debugged the code like
fetch(smth).then(this.processResponse);
when the response handler was a method and certainlythis
reference got lost. It's possible tobind
the instance method to a correctthis
reference or wrap the original one in the arrow wrapper, but in this case, we will produce new functions during the run.So to my mind, this question has no straight response without doing the math in the exact use case you work on. You should compare the counts of how many times the binding will be called (if you use the method and then bind when needed) or duplicate functions will be created (if you instantiate an arrow function)