Hey everyone 👋🏻,
In this article, let us understand When and when not should we use arrow functions.
Arrow Functions
With ES6, JavaScript has evolved a lot because of the customizations that the new standard brings to the language. It has immensely improved our development experienced with the introduction of Next Generation JavaScript features. One of such features that ES6 brings to the table is the Arrow Functions.
The purpose of writing this article is to make the reader aware about the scenarios where he/she should not use arrow functions and instead make use of the good old school function syntax or the function expression syntax. We need to be a bit careful as well while using arrow functions because they can affect the readability of our code as well.
When to use Arrow Functions ?
There are some solid reasons why using arrow functions can be a plus.
- Securing the scope : The arrow functions remember their context in which they are defined in and they stick to that.
So even if we introduce a normal callback function and mix that with a couple of arrow functions, then that can lead to messing up of the scope. - Compact : Easier to read and write. Just a fat arrow, and they can be anonymous as well.
- Code Clarity: When every function in your code is an arrow function, then a normal function is the one that we rely on for defining the scope. So we as developers can always look up the next higher order function statement to figure out the this keyword.
1. Defining methods on an object
In JavaScript, as we know a method is a function that we set in a property of the object. So right at the time of invoking the method, the this keyword points to the object that the method belongs to.
Now let us talk about Object Literals here. Let us say we want to define a method inside an object literal. At first, you might be tempted to use an arrow function for a method definition within the object literal. But let us understand with the help of an example, why you should not use an arrow function within the object literals.
const user = { stocks : [ { stock : 'Amazon', price_in_dollars : 937, units_bought: 5 }, { stock : 'Tesla', price_in_dollars : 652, units_bought: 3 }, { stock : 'Meta Platforms', price_in_dollars : 465, units_bought: 2 } ], get_total_invested : () => { console.log(this === window); return this.stocks.reduce((acc, stock) => acc + stock.price_in_dollars * stock.units_bought, 0); } }; console.log(this === window); const invested_amount = user.get_total_invested(); console.log(invested_amount);
user.get_total_invested
method as we can see is defined within an arrow function. But if you try the above code, the invocation to user.get_total_invested
would throw a TypeError
because in the above case this.stocks
would resolved to undefined.
Now the thing to note here is that when we are invoking the get_total_invested
method on the user
object, the context in which it is called in is the window
object which we can easily verify by comparing this
keyword with the window
object in the get_total_invested
method as you can see within this method, the this
keyword points to the window
object. So in essence, this.stocks
resolves to window.stocks
and this ultimately gives us undefined
because we don’t have any stocks
property on the window
object.
In order to solve this, we can simply switch to the function expression syntax or its shorthand for method definition. For this case, at the invocation point, this
keyword would refer to the the thing to the left of the dot at the invocation point. Here is the code with the above modifications :
const user = { stocks : [ { stock : 'Amazon', price_in_dollars : 937, units_bought: 5 }, { stock : 'Tesla', price_in_dollars : 652, units_bought: 3 }, { stock : 'Meta Platforms', price_in_dollars : 465, units_bought: 2 } ], get_total_invested(){ console.log(this === user); return this.stocks.reduce((acc, stock) => acc + stock.price_in_dollars * stock.units_bought, 0); } }; const invested_amount = user.get_total_invested(); console.log(invested_amount);
Now in the above code, get_total_invested
is a method that is being called on the user
object. So with the regular function syntax, the thing to the left of dot which is the user
object here is what this
resolves to in the end and therefore the above code would work fine. Here is the output for the above code :
From the output, you can also easily decipher that the this
keyword indeed points to the user
object and hence this === user
returns true as a result.
Let us now move forward and take arrow functions with Object Prototype
function User(name, age, profession) { this.name = name; this.age = age; this.profession = profession; } User.prototype.sayHello = () => { console.log(this === window); return `Hello ${this.name}`; } const user = new User('Alex',24,'Developer'); console.log(user.sayHello());
Now here also the same thing holds true as discussed above. In the above code, instead of using the arrow function syntax for defining our sayHello
method where the this
keyword points to window
we should instead use the function expression syntax to solve this. So here is the modified code :
function User(name, age, profession) { this.name = name; this.age = age; this.profession = profession; } User.prototype.sayHello = function(){ console.log(this === window); return `Hello ${this.name}`; } const user = new User('Alex',24,'Developer'); console.log(user.sayHello());
So with just this simple change, sayHello
regular function is changing its context to user
object when called as a method : user.sayHello()
.
2. Arrow Functions cannot be used as constructors
For constructors, use regular functions otherwise JavaScript will throw an error if it encounters an arrow function getting used as a constructor
. This means that JavaScript implicitly prevents from doing that by throwing an exception.
Let us go a bit deeper inside and try to understand a bit more on this :
In simple words, the objects (or the “constructors”) rely on the this
keyword to be able to be able to be modified.
The this
keyword always references the global object which is the window
object.
So if we have something like this :
const User = (name, age, profession) => { this.name = name; this.age = age; this.profession = profession; } const user = new User("Alex", 21, "developer");
Executing new User("Alex", 21, "developer")
, where User
is an arrow function, JavaScript throws a TypeError
that User
cannot be used as a constructor.
But, instead if you do something like this :
const User = function(name, age, profession) { this.name = name; this.age = age; this.profession = profession; } const user = new User('Alex',21,'developer'); console.log(user.name);
Now here, the new
keyword does some magic under the hood and makes the this
keyword that is inside the User
to initially first refer to the empty object instead of referencing to the global object like it earlier did. Right after this, new properties like name
,age
and gender
get created inside this empty object pointed to by the this
keyword and the values will be Alex
,21
and developer
respectively. Finally, the object pointed to by the this
keyword gets returned.
Now a thing to note here is that arrow functions do not allow their this
object to be modified. Their this
object is always the one from the scope where they were statically created. This is called Static Lexical Scope. This is precisely the reason that why we cannot do operations like bind
, apply
, or call
with arrow functions. Simply, their this
is locked to the value of the this of
the scope where they were created.
3. Arrow Functions as callback functions
As we know in client side JavaScript, attaching event listeners to the DOM elements is a very common task. An event triggers the handler function with this
as the target element.
So for example, let us say we have this code
const button = document.querySelector('button'); button.addEventListener('click',() => { console.log(this === window); this.innerHTML = 'Changed'; });
Now as discussed just a while back, this
points to the window
object in an arrow function that is defined in the global context. So whenever a click event occurs, browser tries to execute the handler function with the button
as context, but arrow functions still stays bound to its pre-defined context.
In above code, this.innerHTML
is same as window.innerHTML
which does nothing infact.
In order to solve this, we need to make use of the function expression syntax which will allow us to change this
depending on the target element.
const button = document.querySelector('button'); button.addEventListener('click', function() { console.log(this === button); this.innerHTML = 'Changed'; });
Now for the above code, this
keyword will infact point to the button
. Therefore, this.innerHTML
will correctly modify the button text and reflect the same in the UI.
So this is it for this article. Thanks for reading.