Personally, I think arrow functions are one of the most awesome syntax additions to the JavaScript language introduced in the ES6 specification — my opinion, by the way. I’ve gotten to use them almost every day since I knew about them, and I guess that goes for most JavaScript developers.
Arrow functions can be used in so many ways as regular JavaScript functions. However, they are commonly used wherever an anonymous function expression is required — for example, as callback functions.
The following example shows how an arrow function can be used as a callback function, especially with array methods like map()
, filter()
, reduce()
, sort()
, etc.
const scores = [ /* ...some scores here... */ ]; const maxScore = Math.max(...scores); // Arrow Function as .map() callback scores.map(score => +(score / maxScore).toFixed(2));
At first glance, it may seem like arrow functions can be used or defined in every way a regular JavaScript function can, but that is not true. Arrow functions, for very good reasons, are not meant to behave exactly the same way as regular JavaScript functions. Perhaps arrow functions can be considered JavaScript functions with anomalies.
Although arrow functions have a pretty simple syntax, that will not be the focus of this article. This article aims to expose the major ways in which arrow functions behave differently from regular functions and how that knowledge can be used to the developer’s advantage.
Please note: Throughout this article, I use the term regular function or regular JavaScript function to refer to a traditional JavaScript function statement or expression defined using the function keyword.
arguments
binding. However, they have access to the arguments object of the closest non-arrow parent function. Named and rest parameters are heavily relied upon to capture the arguments passed to arrow functions.Functions in JavaScript are usually defined with named parameters. Named parameters are used to map arguments to local variables within the function scope based on position.
Consider the following JavaScript function:
function logParams (first, second, third) { console.log(first, second, third); } // first => 'Hello' // second => 'World' // third => '!!!' logParams('Hello', 'World', '!!!'); // "Hello" "World" "!!!" // first => { o: 3 } // second => [ 1, 2, 3 ] // third => undefined logParams({ o: 3 }, [ 1, 2, 3 ]); // {o: 3} [1, 2, 3]
The logParams()
function is defined with three named parameters: first
, second
, and third
. The named parameters are mapped to the arguments with which the function was called based on position. If there are more named parameters than the arguments passed to the function, the remaining parameters are undefined
.
Regular JavaScript functions exhibit a strange behavior in non-strict mode with regards to named parameters. In non-strict mode, regular JavaScript functions allow duplicate named parameters. The following code snippet shows the consequence of that behavior:
function logParams (first, second, first) { console.log(first, second); } // first => 'Hello' // second => 'World' // first => '!!!' logParams('Hello', 'World', '!!!'); // "!!!" "World" // first => { o: 3 } // second => [ 1, 2, 3 ] // first => undefined logParams({ o: 3 }, [ 1, 2, 3 ]); // undefined [1, 2, 3]
As we can see, the first
parameter is a duplicate; thus, it is mapped to the value of the third argument passed to the function call, completely overriding the first argument passed. This is not a desirable behavior.
The good news is that this behavior is not allowed in strict mode. Defining a function with duplicate parameters in strict mode will throw a Syntax Error
indicating that duplicate parameters are not allowed.
// Throws an error because of duplicate parameters (Strict mode) function logParams (first, second, first) { "use strict"; console.log(first, second); }
Now here is something about arrow functions:
Unlike regular functions, arrow functions do not allow duplicate parameters, whether in strict or non-strict mode. Duplicate parameters will cause a
Syntax Error
to be thrown.
// Always throws a syntax error const logParams = (first, second, first) => { console.log(first, second); }
Function overloading is the ability to define a function such that it can be invoked with different call signatures (shapes or number of arguments). The good thing is that the arguments binding for JavaScript functions makes this possible.
For a start, consider this very simple overloaded function that calculates the average of any number of arguments passed to it:
function average() { // the number of arguments passed const length = arguments.length; if (length == 0) return 0; // convert the arguments to a proper array of numbers const numbers = Array.prototype.slice.call(arguments); // a reducer function to sum up array items const sumReduceFn = function (a, b) { return a + Number(b) }; // return the sum of array items divided by the number of items return numbers.reduce(sumReduceFn, 0) / length; }
I have tried to make the function definition as verbose as possible so that its behavior can be clearly understood. The function can be called with any number of arguments from zero to the max number of arguments that a function can take — that should be 255.
Here are some results from calls to the average()
function:
average(); // 0 average('3o', 4, 5); // NaN average('1', 2, '3', 4, '5', 6, 7, 8, 9, 10); // 5.5 average(1.75, 2.25, 3.5, 4.125, 5.875); // 3.5
Now try to replicate the average()
function using the arrow function syntax. I mean, how difficult can that be? First guess — all you have to do is this:
const average = () => { const length = arguments.length; if (length == 0) return 0; const numbers = Array.prototype.slice.call(arguments); const sumReduceFn = function (a, b) { return a + Number(b) }; return numbers.reduce(sumReduceFn, 0) / length; }
When you test this function now, you realize that it throws a Reference Error
, and guess what? Of all the possible causes, it is complaining that arguments
is not defined.
Now here is something else about arrow functions:
Unlike regular functions, the
arguments
binding does not exist for arrow functions. However, they have access to thearguments
object of a non-arrow parent function.
Based on this understanding, you can modify the average()
function to be a regular function that will return the result of an immediately invoked nested arrow function, which should have access to the arguments
of the parent function. This will look like this:
function average() { return (() => { const length = arguments.length; if (length == 0) return 0; const numbers = Array.prototype.slice.call(arguments); const sumReduceFn = function (a, b) { return a + Number(b) }; return numbers.reduce(sumReduceFn, 0) / length; })(); }
Obviously, that solved the problem you had with the arguments
object not being defined. However, you had to use a nested arrow function inside a regular function, which seems rather unnecessary for a simple function like this.
Since accessing the arguments
object is obviously the problem here, is there an alternative? The answer is yes. Say hello to ES6 rest parameters.
With ES6 rest parameters, you can get an array that gives you access to all or part of the arguments that were passed to a function. This works for all function flavors, whether regular functions or arrow functions. Here is what it looks like:
const average = (...args) => { if (args.length == 0) return 0; const sumReduceFn = function (a, b) { return a + Number(b) }; return args.reduce(sumReduceFn, 0) / args.length; }
Wow! Rest parameters to the rescue — you finally arrived at an elegant solution for implementing the average()
function as an arrow function.
There are some caveats against relying on rest parameters for accessing function arguments:
arguments
object inside the function. The rest parameter is an actual function parameter, while the arguments
object is an internal object bound to the scope of the function.arguments
object of the function always captures all the function’s arguments.arguments
object points to an array-like object containing all the function’s arguments.Before you proceed, consider another very simple overloaded function that converts a number from one number base to another. The function can be called with one to three arguments. However, when it is called with two arguments or fewer, it swaps the second and third function parameters in its implementation.
Here is what it looks like with a regular function:
function baseConvert (num, fromRadix = 10, toRadix = 10) { if (arguments.length < 3) { // swap variables using array destructuring [toRadix, fromRadix] = [fromRadix, toRadix]; } return parseInt(num, fromRadix).toString(toRadix); }
Here are some calls to the baseConvert()
function:
// num => 123, fromRadix => 10, toRadix => 10 console.log(baseConvert(123)); // "123" // num => 255, fromRadix => 10, toRadix => 2 console.log(baseConvert(255, 2)); // "11111111" // num => 'ff', fromRadix => 16, toRadix => 8 console.log(baseConvert('ff', 16, 8)); // "377"
Based on what you know about arrow functions not having an arguments
binding of their own, you can rewrite the baseConvert()
function using the arrow function syntax as follows:
const baseConvert = (num, ...args) => { // destructure the `args` array and // set the `fromRadix` and `toRadix` local variables let [fromRadix = 10, toRadix = 10] = args; if (args.length < 2) { // swap variables using array destructuring [toRadix, fromRadix] = [fromRadix, toRadix]; } return parseInt(num, fromRadix).toString(toRadix); }
Notice in the previous code snippets that I have used the ES6 array destructuring syntax to set local variables from array items and also to swap variables. You can learn more about destructuring by reading this guide: “ES6 Destructuring: The Complete Guide.”
A regular JavaScript function can be called with the new
keyword, for which the function behaves as a class constructor for creating new instance objects.
Here is a simple example of a function being used as a constructor:
function Square (length = 10) { this.length = parseInt(length) || 10; this.getArea = function() { return Math.pow(this.length, 2); } this.getPerimeter = function() { return 4 * this.length; } } const square = new Square(); console.log(square.length); // 10 console.log(square.getArea()); // 100 console.log(square.getPerimeter()); // 40 console.log(typeof square); // "object" console.log(square instanceof Square); // true
When a regular JavaScript function is invoked with the new
keyword, the function’s internal [[Construct]]
method is called to create a new instance object and allocate memory. After that, the function body is executed normally, mapping this
to the newly created instance object. Finally, the function implicitly returns this
(the newly created instance object), except a different return value has been specified in the function definition.
Also, all regular JavaScript functions have a prototype
property. The prototype
property of a function is an object that contains properties and methods that are shared among all instance objects created by the function when used as a constructor.
Initially, the prototype
property is an empty object with a constructor
property that points to the function. However, it can be augmented with properties and methods to add more functionality to objects created using the function as a constructor.
Here is a slight modification of the previous Square
function that defines the methods on the function’s prototype instead of the constructor itself.
function Square (length = 10) { this.length = parseInt(length) || 10; } Square.prototype.getArea = function() { return Math.pow(this.length, 2); } Square.prototype.getPerimeter = function() { return 4 * this.length; } const square = new Square(); console.log(square.length); // 10 console.log(square.getArea()); // 100 console.log(square.getPerimeter()); // 40 console.log(typeof square); // "object" console.log(square instanceof Square); // true
As you can tell, everything still works as expected. In fact here, is a little secret: ES6 classes do something similar to the above code snippet on the background — they are simply syntactic sugar.
Do they also share this behavior with regular JavaScript functions? The answer is no. Now here, again, is something else about arrow functions:
Unlike regular functions, arrow functions can never be called with the new keyword because they do not have the
[[Construct]]
method. As such, theprototype
property also does not exist for arrow functions.
Sadly, that is very true. Arrow functions cannot be used as constructors. They cannot be called with the new
keyword. Doing that throws an error indicating that the function is not a constructor.
As a result, bindings such as new.target
that exist inside functions that can be called as constructors do not exist for arrow functions; instead, they use the new.target
value of the closest non-arrow parent function.
Also, because arrow functions cannot be called with the new
keyword, there is really no need for them to have a prototype. Hence, the prototype
property does not exist for arrow functions.
Since the prototype
of an arrow function is undefined
, attempting to augment it with properties and methods, or access a property on it, will throw an error.
const Square = (length = 10) => { this.length = parseInt(length) || 10; } // throws an error const square = new Square(5); // throws an error Square.prototype.getArea = function() { return Math.pow(this.length, 2); } console.log(Square.prototype); // undefined
this
?If you have been writing JavaScript programs for some time now, you would have noticed that every invocation of a JavaScript function is associated with an invocation context depending on how or where the function was invoked.
The value of this
inside a function is heavily dependent on the invocation context of the function at call time, which usually puts JavaScript developers in a situation where they have to ask themselves the famous question: What is the value of this
?
Here is a summary of what the value of this
points to for different kinds of function invocations:
new
keyword: this
points to the new instance object created by the internal [[Construct]]
method of the function. this
(the newly created instance object) is usually returned by default, except a different return value was explicitly specified in the function definition.new
keyword: In non-strict mode, this
points to the global object of the JavaScript host environment (in a web browser, this is usually the window
object). However, in strict mode, the value of this
is undefined
; thus, trying to access or set a property on this
will throw an error.Function.prototype
object provides three methods that make it possible for functions to be bound to an arbitrary object when they are called, namely: call()
, apply()
, and bind()
. When the function is called using any of these methods, this
points to the specified bound object.this
points to the object on which the function (method) was invoked regardless of whether the method is defined as an own property of the object or resolved from the object’s prototype chain.this
points to the target object, DOM element, document
, or window
on which the event was fired.For a start, consider this very simple JavaScript function that will be used as a click event listener for, say, a form submit button:
function processFormData (evt) { evt.preventDefault(); // get the parent form of the submit button const form = this.closest('form'); // extract the form data, action and method const data = new FormData(form); const { action: url, method } = form; // send the form data to the server via some AJAX request // you can use Fetch API or jQuery Ajax or native XHR } button.addEventListener('click', processFormData, false);
If you try this code, you will see that everything works correctly. The value this
inside the event listener function, like you saw earlier, is the DOM element on which the click event was fired, which in this case is button
.
Therefore, it is possible to point to the parent form of the submit button using:
this.closest('form');
At the moment, you are using a regular JavaScript function as the event listener. What happens if you change the function definition to use the all-new arrow function syntax?
const processFormData = (evt) => { evt.preventDefault(); const form = this.closest('form'); const data = new FormData(form); const { action: url, method } = form; // send the form data to the server via some AJAX request // you can use Fetch API or jQuery Ajax or native XHR } button.addEventListener('click', processFormData, false);
If you try this now, you will notice that you are getting an error. From the look of things, it seems the value of this
isn’t what you were expecting. For some reason, this
no longer points to the button
element — instead, it points to the global window
object.
this
binding?Do you remember Function.prototype.bind()
? You can use that to force the value of this
to be bound to the button
element when you are setting up the event listener for the submit button. Here it is:
// Bind the event listener function (`processFormData`) to the `button` element button.addEventListener('click', processFormData.bind(button), false);
Oops! It seems that was not the fix you were looking for. this
still points to the global window
object. Is this a problem peculiar to arrow functions? Does that mean arrow functions cannot be used for event handlers that rely on this
?
Now here is the last thing we’ll cover about arrow functions:
Unlike regular functions, arrow functions do not have a
this
binding of their own. The value ofthis
is resolved to that of the closest non-arrow parent function or the global object otherwise.
This explains why the value of this
in the event listener arrow function points to the window object (global object). Since it was not nested within a parent function, it uses the this value from the closest parent scope, which is the global scope.
This, however, does not explain why you cannot bind the event listener arrow function to the button
element using bind()
. Here comes an explanation for that:
Unlike regular functions, the value of
this
inside arrow functions remains the same and cannot change throughout their lifecycle, irrespective of the invocation context.
This behavior of arrow functions makes it possible for JavaScript engines to optimize them since the function bindings can be determined beforehand.
Consider a slightly different scenario in which the event handler is defined using a regular function inside an object’s method and also depends on another method of the same object:
({ _sortByFileSize: function (filelist) { const files = Array.from(filelist).sort(function (a, b) { return a.size - b.size; }); return files.map(function (file) { return file.name; }); }, init: function (input) { input.addEventListener('change', function (evt) { const files = evt.target.files; console.log(this._sortByFileSize(files)); }, false); } }).init(document.getElementById('file-input'));
Here is a one-off object literal with a _sortByFileSize()
method and an init()
method, which is invoked immediately. The init()
method takes a file input
element and sets up a change event handler for the input element that sorts the uploaded files by file size and logs them on the browser’s console.
If you test this code, you will realize that when you select files to upload, the file list doesn’t get sorted and logged to the console; instead, an error is thrown on the console. The problem comes from this line:
console.log(this._sortByFileSize(files));
Inside the event listener function, this
points to the DOM element on which the event was fired, which in this case is the input
element; hence this._sortByFileSize
is undefined.
To solve this problem, you need to bind this
inside the event listener to the outer object containing the methods so that you can be able to call this._sortByFileSize()
. Here, you can use bind()
as follows:
init: function (input) { input.addEventListener('change', (function (evt) { const files = evt.target.files; console.log(this._sortByFileSize(files)); }).bind(this), false); }
Now everything works as expected. Instead of using bind()
here, you could simply replace the event listener regular function with an arrow function. The arrow function will use the this
value from the parent init()
method, which will be the required object.
init: function (input) { input.addEventListener('change', evt => { const files = evt.target.files; console.log(this._sortByFileSize(files)); }, false); }
Before you proceed, consider one more scenario. Let’s say you have a simple timer function that can be invoked as a constructor to create countdown timers in seconds. It uses setInterval()
to keep counting down until the duration elapses or until the interval is cleared. Here it is:
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval(function () { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }, 1000); } const timer = new Timer(30);
If you run this code, you will see that the countdown timer seems to be broken. It keeps logging NaN
on the console infinitely.
The problem here is that inside the callback function passed to setInterval()
, this
points to the global window
object instead of the newly created instance
object within the scope of the Timer()
function. Hence, both this.seconds
and this.interval
are undefined
.
As before, to fix this, you can use bind()
to bind the value of this
inside the setInterval()
callback function to the newly created instance object as follows:
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval((function () { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }).bind(this), 1000); }
Or, better still, you can replace the setInterval()
callback regular function with an arrow function so that it can use the value of this
from the closest non-arrow parent function, which is Timer
in this case.
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval(() => { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }, 1000); }
Now that you completely understand how arrow functions handle the this
keyword, it is important to note that an arrow function will not be ideal for cases where you need the value of this
to be preserved — for example, when defining object methods that need a reference to the object or augmenting a function’s prototype with methods that need a reference to the target object.
Throughout this article, you have seen several bindings that are available inside regular JavaScript functions but don’t exist for arrow functions. Instead, arrow functions derive the values of such bindings from their closest non-arrow parent function.
In summary, here is a list of the nonexistent bindings in arrow functions:
arguments
: List of arguments passed to the function when it is callednew.target
: A reference to the function being called as a constructor with the new
keywordsuper
: A reference to the prototype of the object to which the function belongs, provided it is defined as a concise object methodthis
: A reference to the invocation context object for the functionHey, I’m really glad that you made it to the end of this article despite the long read time, and I strongly hope that you learned a thing or two while reading it. Thanks for your time.
JavaScript arrow functions are really awesome and have these cool characteristics (which we’ve reviewed in this article) that will make it easy for JavaScript engineers to optimize them in ways that they can’t for regular JavaScript functions.
In my opinion, I would say that you should keep using arrow functions as much as you can — except in cases where you just can’t.
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 nowIn this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
SOLID 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.
3 Replies to "Anomalies in JavaScript arrow functions"
Excellent post!
I use arrow functions only when it makes semantic or functional sense.
Missing writing about hoisting and at this point it makes no sense to use arrow functions as a first option …
Amazing deep dive, i always thought arrow funcs to be just syntactic sugar until now. Thanks!
Thank you for the detailed article. It is extremely helpful and I learned a ton <3