In JavaScript, an arrow function expression is a compact alternative to a traditional function expression. I would argue that most people use them for their brevity, which is absolutely fine. But they differ from traditional function expressions in a few important ways, most notably in how they handle the this
keyword.
In the following code snippet, the timer
constant is assigned to an object literal that represents a countdown timer. The object literal has two properties: ms
with a value of 5000
, and interval
with a value of 1000
. The ms
property represents the total number of milliseconds for the countdown, while the interval
property represents the number of milliseconds between each tick. The object literal also has a tick()
method that starts the countdown. But it’s broken! Can you figure out why?
const timer = {
ms: 5000,
interval: 1000,
tick() {
console.log(this.ms);
const interval = setInterval(function () {
this.ms -= this.interval;
console.log(this.ms);
if (this.ms === 0) clearInterval(interval);
}, this.interval);
return interval;
},
};
timer.tick(); // 5000, NaN, ..., NaN
The problem is that the the setInterval()
method invokes its callback function in a separate execution context from the tick()
method. This means the value of the callback function’s this
keyword is not the object referenced by the timer
constant. In the browser, it’s the global object, i.e. the window
object. In Node.js, it’s the instance of the Timeout
class created internally by the setInterval()
method.
When the tick()
method is invoked, it logs the initial value of the ms
property, which is 5000
. Because the callback function for the setInterval()
method is invoked in a separate execution context, it tries to access the ms
and interval
properties on the window
object (in the browser) or the Timeout
instance (in Node.js). Both of these have the value undefined
, and undefined - undefined
evaluates to NaN
(not a number). The condition of the if
statement is never truthy (because undefined
is never strictly equal to 0
) and so the interval keeps running indefinitely.
You can fix this problem by replacing the traditional function expression with an arrow function expression. This works because arrow function expressions do not have their own binding to the this
keyword. They always use the value from their lexical scope, which is basically the scope in which they’re defined. In this example, the arrow function expression is defined in the lexical scope of the tick()
method, where the value of the this
keyword is the object referenced by the timer
constant.
const timer = {
ms: 5000,
interval: 1000,
tick() {
console.log(this.ms);
const interval = setInterval(() => {
this.ms -= this.interval;
console.log(this.ms);
if (this.ms === 0) clearInterval(interval);
}, this.interval);
return interval;
},
};
timer.tick(); // 5000, 4000, 3000, 2000, 1000, 0
So, aside from brevity, when should you use an arrow function expression in JavaScript? When you need the value of a function’s this
keyword to be the same one from its surrounding (lexical) scope.
For more on the this
keyword, check out You Don’t Know JS by Kyle Simpson.