Arrow functions revisited
You met arrow functions early on as a compact way to write a function. Time to look again, because the shortness is the least interesting thing about them.
An arrow function isn’t merely a regular function with fewer characters. It behaves differently in ways that matter. The big one: it doesn’t get its own this. That single design choice is what makes arrows the natural fit for a very common JavaScript pattern.
That pattern is: write a tiny function and hand it to someone else to run.
arr.forEach(func)—forEachcallsfunconce per array element.setTimeout(func, ms)— the browser’s scheduler callsfunclater.arr.map,arr.filter, promise.then— same idea, a function you pass in and something else invokes.
Passing functions around like this is everyday JavaScript. And when you write one of those small passed-in functions, you almost always want it to keep working inside the current context — the same this, the same variables — not spin up a fresh one of its own. Arrow functions give you exactly that.
Arrow functions have no “this”
Back in Object methods, “this” you saw that arrow functions don’t have their own this. When code inside an arrow reads this, the value comes from the surrounding scope — wherever the arrow was written, not wherever it’s called from.
This is precisely what you want when looping inside a method:
let group = {
title: "Design Club",
students: ["Maya", "Raj", "Nadia"],
showList() {
this.students.forEach(
student => alert(this.title + ': ' + student)
);
}
};
group.showList();
Inside forEach, the arrow’s this is the same this as in showList, which is group. So this.title resolves to "Design Club". The arrow reaches outward and borrows the method’s this.
Now watch it break with a regular function:
let group = {
title: "Design Club",
students: ["Maya", "Raj", "Nadia"],
showList() {
this.students.forEach(function(student) {
// Error: Cannot read property 'title' of undefined
alert(this.title + ': ' + student);
});
}
};
group.showList();
A regular function gets its this decided by how it’s called, not where it’s written. forEach invokes your callback as a plain function with no object in front of it, so inside that callback this is undefined (in strict mode, which modules use). Then undefined.title throws.
The arrow version sidesteps the whole problem: it has no this slot of its own to be set to undefined, so the lookup walks outward and finds showList’s this.
Run both versions on the same object and watch the difference. The arrow callback keeps this.title; the regular callback gets this === undefined (strict mode) and throws the moment it touches .title:
Try it: calling new on a regular function builds an object, but calling it on an arrow throws immediately.
Arrows have no “arguments”
Regular functions get a special array-like arguments object holding every argument passed, whatever the parameter list says. Arrow functions don’t. Reference arguments inside an arrow and, just like this, it resolves to the arguments of the nearest enclosing regular function.
That “reach outward” behavior turns out to be handy for decorators — wrappers that forward a call along with its original this and all its arguments.
Here’s defer(f, ms): it takes a function and hands back a wrapper that runs f after a delay of ms milliseconds, preserving both the this and the arguments of the original call.
function defer(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(who) {
alert('Hello, ' + who);
}
let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("Maya"); // Hello, Maya after 2 seconds
The inner arrow passed to setTimeout has no this and no arguments of its own, so both expressions inside it refer to the wrapper function that surrounds it. That wrapper is the one actually called as sayHiDeferred("Maya"), so its this and its arguments (["Maya"]) are exactly what we forward through f.apply.
Here defer wraps a method. Because the inner arrow borrows the wrapper’s this and arguments, the delayed call still sees the right object and the original argument — no ctx or rest-parameter bookkeeping in sight:
Compare that to writing the same thing with a plain function inside setTimeout:
function defer(f, ms) {
return function(...args) {
let ctx = this;
setTimeout(function() {
return f.apply(ctx, args);
}, ms);
};
}
Now the inner function has its own this and arguments, which would be the wrong ones. So you’re forced to capture the outer values into extra variables — ctx for this, and args (via rest parameters, since you can’t just reach for the outer arguments) — and then thread them into apply. The arrow version deletes all of that bookkeeping.
Summary
Arrow functions differ from regular functions in four concrete ways. They don’t have their own:
this— read from the enclosing scope instead.arguments— also read from the enclosing scope.new— they can’t be used as constructors and throw if you try.super— the same lexical borrowing applies. You’ll seesuperin Class inheritance.
None of that is a limitation to work around. It’s the point. Arrows are built for short pieces of code that don’t want a context of their own and would rather operate inside the one around them — callbacks, in other words. For that job they’re the cleanest tool you have.