Arrow functions vs regular functions in JavaScript
The real differences between arrow and regular functions in JavaScript: this, arguments, new, hoisting and object literals, and when to use each.
The main difference is this. An arrow function doesn't have its own this; it uses the this of the code around it, where it was defined. A regular function gets this from how it's called. Arrows also have no arguments, can't be used with new, and aren't hoisted like function declarations.
const counter = {
count: 0,
startRegular() {
setTimeout(function () {
console.log(this.count); // undefined
});
},
startArrow() {
setTimeout(() => {
console.log(this.count); // 0
});
},
};
counter.startRegular();
counter.startArrow();The regular callback is called by the timer, not by counter, so its this is something else (window in browsers). The arrow uses the this of startArrow(), which is counter. Before arrows, people wrote const self = this or .bind(this) to work around it.
Object methods written as arrows break
The flip side: don't write object methods as arrows. The arrow takes this from the code around the object, not from the object:
const user = {
firstName: 'Ada',
greet() {
return `Hi, ${this.firstName}`;
},
greetArrow: () => `Hi, ${this.firstName}`,
};
user.greet(); // 'Hi, Ada'
user.greetArrow(); // 'Hi, undefined'In an ES module, the top-level this is undefined, so greetArrow() throws a TypeError instead.
Event listeners
In a regular function listener, this is the element the listener is attached to. In an arrow, it isn't, so use event.currentTarget:
button.addEventListener('click', function () {
this.classList.toggle('is-active');
});
button.addEventListener('click', (event) => {
event.currentTarget.classList.toggle('is-active');
});I prefer the second version, because it works the same no matter how the function is written.
No arguments, no new, no prototype
Arrows don't have their own arguments object. Inside an arrow, arguments refers to the enclosing regular function's arguments, which is rarely what you want. Use rest parameters, which work in both kinds of functions:
const sum = (...numbers) => numbers.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6
const User = (name) => ({ name });
new User('Ada'); // TypeError: User is not a constructor
User.prototype; // undefinedYou can't use an arrow as a constructor. For that, use a class.
Hoisting
Function declarations are hoisted, so you can call them before the line where they're defined. Arrows and function expressions assigned to const can't be used before that line:
greet('Ada'); // 'Hi, Ada'
function greet(name) {
return `Hi, ${name}`;
}
shout('Ada'); // ReferenceError
const shout = (name) => `HI, ${name.toUpperCase()}`;The error says "Cannot access 'shout' before initialization". This only bites when the call runs before the definition. Calling shout() inside another function that runs later is fine.
Returning an object literal
After =>, a curly brace starts a function body, not an object. Wrap the object in parentheses:
const toUserBroken = (name) => { name }; // returns undefined
const toUser = (name) => ({ name }); // returns { name }Class fields as arrows
An arrow in a class field is created for each instance, with this fixed to that instance. It keeps working when you pass the method around as a callback, which is why it's common for event handlers:
class Toggle {
isOn = false;
handleClick = () => {
this.isOn = !this.isOn;
};
}
const toggle = new Toggle();
button.addEventListener('click', toggle.handleClick); // worksA normal method passed like that would lose its this. The trade-off is that each instance gets its own copy of the function instead of sharing one on the prototype.
Which one to use
- Arrows for callbacks:
map,filter,then,setTimeout, and event handlers that useevent.currentTarget. - Method shorthand (
greet() {}) for object and class methods. classfor anything you'd call withnew.- For standalone named functions it's mostly style. I use
functiondeclarations for top-level helpers, because they're hoisted and easy to spot, and arrows everywhere else.